-
-
Notifications
You must be signed in to change notification settings - Fork 6.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[codegen][validation] Add support for 'null' type #5290
Changes from 26 commits
eb31144
56da622
00718d4
8ff9236
7cbcba0
30111a5
68bbe2d
ed14375
2059035
ca257f4
bb91104
57f4a0e
36b8d62
71363d5
660e4dc
0f140cc
8ba134d
23c8bcc
ae76186
3e52b6f
e18ddc3
3b61fb9
68ad45d
4d6e09d
364d355
a9d212d
e825bbc
023cd47
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -266,6 +266,18 @@ private static void visitContent(OpenAPI openAPI, Content content, OpenAPISchema | |
} | ||
} | ||
|
||
/** | ||
* Invoke the specified visitor function for every schema that matches mimeType in the OpenAPI document. | ||
* | ||
* To avoid infinite recursion, referenced schemas are visited only once. When a referenced schema is visited, | ||
* it is added to visitedSchemas. | ||
* | ||
* @param openAPI the OpenAPI document that contains schema objects. | ||
* @param schema the root schema object to be visited. | ||
* @param mimeType the mime type. TODO: does not seem to be used in a meaningful way. | ||
* @param visitedSchemas the list of referenced schemas that have been visited. | ||
* @param visitor the visitor function which is invoked for every visited schema. | ||
*/ | ||
private static void visitSchema(OpenAPI openAPI, Schema schema, String mimeType, List<String> visitedSchemas, OpenAPISchemaVisitor visitor) { | ||
visitor.visit(schema, mimeType); | ||
if (schema.get$ref() != null) { | ||
|
@@ -648,13 +660,41 @@ public static Schema getSchema(OpenAPI openAPI, String name) { | |
return getSchemas(openAPI).get(name); | ||
} | ||
|
||
/** | ||
* Return a Map of the schemas defined under /components/schemas in the OAS document. | ||
* The returned Map only includes the direct children of /components/schemas in the OAS document; the Map | ||
* does not include inlined schemas. | ||
* | ||
* @param openAPI the OpenAPI document. | ||
* @return a map of schemas in the OAS document. | ||
*/ | ||
public static Map<String, Schema> getSchemas(OpenAPI openAPI) { | ||
if (openAPI != null && openAPI.getComponents() != null && openAPI.getComponents().getSchemas() != null) { | ||
return openAPI.getComponents().getSchemas(); | ||
} | ||
return Collections.emptyMap(); | ||
} | ||
|
||
/** | ||
* Return the list of all schemas in the 'components/schemas' section of an openAPI specification, | ||
* including inlined schemas and children of composed schemas. | ||
* | ||
* @param openAPI OpenAPI document | ||
* @return a list of schemas | ||
*/ | ||
public static List<Schema> getAllSchemas(OpenAPI openAPI) { | ||
List<Schema> allSchemas = new ArrayList<Schema>(); | ||
List<String> refSchemas = new ArrayList<String>(); | ||
getSchemas(openAPI).forEach((key, schema) -> { | ||
// Invoke visitSchema to recursively visit all schema objects, included inlined and composed schemas. | ||
// Use the OpenAPISchemaVisitor visitor function | ||
visitSchema(openAPI, schema, null, refSchemas, (s, mimetype) -> { | ||
allSchemas.add(s); | ||
}); | ||
}); | ||
return allSchemas; | ||
} | ||
|
||
/** | ||
* If a RequestBody contains a reference to an other RequestBody with '$ref', returns the referenced RequestBody if it is found or the actual RequestBody in the other cases. | ||
* | ||
|
@@ -971,7 +1011,8 @@ public static List<Schema> getInterfaces(ComposedSchema composed) { | |
*/ | ||
public static String getParentName(ComposedSchema composedSchema, Map<String, Schema> allSchemas) { | ||
List<Schema> interfaces = getInterfaces(composedSchema); | ||
|
||
int nullSchemaChildrenCount = 0; | ||
boolean hasAmbiguousParents = false; | ||
List<String> refedWithoutDiscriminator = new ArrayList<>(); | ||
|
||
if (interfaces != null && !interfaces.isEmpty()) { | ||
|
@@ -988,19 +1029,34 @@ public static String getParentName(ComposedSchema composedSchema, Map<String, Sc | |
return parentName; | ||
} else { | ||
// not a parent since discriminator.propertyName is not set | ||
hasAmbiguousParents = true; | ||
refedWithoutDiscriminator.add(parentName); | ||
} | ||
} else { | ||
// not a ref, doing nothing | ||
// not a ref, doing nothing, except counting the number of times the 'null' type | ||
// is listed as composed element. | ||
if (ModelUtils.isNullType(schema)) { | ||
// If there are two interfaces, and one of them is the 'null' type, | ||
// then the parent is obvious and there is no need to warn about specifying | ||
// a determinator. | ||
nullSchemaChildrenCount++; | ||
} | ||
} | ||
} | ||
if (refedWithoutDiscriminator.size() == 1 && nullSchemaChildrenCount == 1) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just a heads up that I don't think this is intended to always indicate polymorphism in the specification. An allOf ref with discriminator is always supposed to indicate polymporphism, but the opposite isn't clear. The case that you've defined here, and that I'm assuming was intended to match the Details:
It doesn't explain this well enough, but from these two it seems like they're defining subtype polymorphism since other types of polymorphism are either irrelevant or don't make much sense in this usage. More discussion (we could turn it into an issue as well): There are additional details of OpenAPI Spec which aren't explained very well with
Even without the discriminator, I don't think the
Unfortunately, this doesn't define whether a codegen should model:
or:
In this issue, there's no clarification: OAI/OpenAPI-Specification#1467 In fact, if you consider from a REST input modeling perspective, it might make sense that all allOf without discriminator are unique types with properties of the I wonder if it may make sense for us to support a vendor extension as a hint whether the user wants to use composition or inheritance for these one-off allOf without a discriminator. Something like:
The latter might be challenging to support the spec requirement that these are validated independently like in the case where two refs have the same property with different validation characteristics. This isn't a case we handle now. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you for the quick feedback. Let me digest this and get back to you tomorrow. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Above did you mean? 1) the generated code; 2) the serialization format of the JSON document; or 3) both? If (1), I agree it's not defined in the OAS spec, but OAS does not mandate how code generators should be implemented. If (2), isn't https://tools.ietf.org/html/draft-handrews-json-schema-02 section 9.2.1.1 mandating that it must be the second case? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I see there are related discussions about defined "subclassOf", but it does not seem to make much progress. I agree with all your points, they will have to be addressed in separate PRs. For example, I had an issue with inlined schema exactly as you mentioned, and I ended up doing a workaround by using a $ref. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh. I wasn't suggesting any additional changes here. I just wanted to comment on the allOf change because of the ambiguity, and hoped it'd generate discussion. There's a lot to be done for allOf support, and I'm hoping we'll make great progress in the 5.0 release. Regarding your comment:
The issue is that JSON Schema focuses on validation only and not on structure. This is further complicated because OpenAPI Specification disassociates it's
I think the only sane way to support it cleanly is by having good defaults and vendor extensions to modify each model's behavior. We may also want to consider a configuration option at generation to define a "global" default. |
||
// One schema is a $ref and the other is the 'null' type, so the parent is obvious. | ||
// In this particular case there is no need to specify a discriminator. | ||
hasAmbiguousParents = false; | ||
} | ||
} | ||
|
||
// parent name only makes sense when there is a single obvious parent | ||
if (refedWithoutDiscriminator.size() == 1) { | ||
LOGGER.warn("[deprecated] inheritance without use of 'discriminator.propertyName' is deprecated " + | ||
"and will be removed in a future release. Generating model for composed schema name: {}. Title: {}", | ||
composedSchema.getName(), composedSchema.getTitle()); | ||
if (hasAmbiguousParents) { | ||
LOGGER.warn("[deprecated] inheritance without use of 'discriminator.propertyName' is deprecated " + | ||
"and will be removed in a future release. Generating model for composed schema name: {}. Title: {}", | ||
composedSchema.getName(), composedSchema.getTitle()); | ||
} | ||
return refedWithoutDiscriminator.get(0); | ||
} | ||
|
||
|
@@ -1080,6 +1136,24 @@ else if (schema instanceof ComposedSchema) { | |
return false; | ||
} | ||
|
||
/** | ||
* Return true if the 'nullable' attribute is set to true in the schema, i.e. if the value | ||
* of the property can be the null value. | ||
* | ||
* In addition, if the OAS document is 3.1 or above, isNullable returns true if the input | ||
* schema is a 'oneOf' composed document with at most two children, and one of the children | ||
* is the 'null' type. | ||
* | ||
* The caller is responsible for resolving schema references before invoking isNullable. | ||
* If the input schema is a $ref and the referenced schema has 'nullable: true', this method | ||
* returns false (because the nullable attribute is defined in the referenced schema). | ||
* | ||
* The 'nullable' attribute was introduced in OAS 3.0. | ||
* The 'nullable' attribute is deprecated in OAS 3.1. In a OAS 3.1 document, the preferred way | ||
* to specify nullable properties is to use the 'null' type. | ||
* | ||
* @param schema the OAS schema. | ||
*/ | ||
public static boolean isNullable(Schema schema) { | ||
if (schema == null) { | ||
return false; | ||
|
@@ -1092,7 +1166,59 @@ public static boolean isNullable(Schema schema) { | |
if (schema.getExtensions() != null && schema.getExtensions().get("x-nullable") != null) { | ||
return Boolean.valueOf(schema.getExtensions().get("x-nullable").toString()); | ||
} | ||
// In OAS 3.1, the recommended way to define a nullable property or object is to use oneOf. | ||
if (schema instanceof ComposedSchema) { | ||
return isNullableComposedSchema(((ComposedSchema) schema)); | ||
} | ||
return false; | ||
} | ||
|
||
/** | ||
* Return true if the specified composed schema is 'oneOf', contains one or two elements, | ||
* and at least one of the elements is the 'null' type. | ||
* | ||
* The 'null' type is supported in OAS 3.1 and above. | ||
* In the example below, the 'OptionalOrder' can have the null value because the 'null' | ||
* type is one of the elements under 'oneOf'. | ||
* | ||
* OptionalOrder: | ||
* oneOf: | ||
* - type: 'null' | ||
* - $ref: '#/components/schemas/Order' | ||
* | ||
* @param schema the OAS composed schema. | ||
* @return true if the composed schema is nullable. | ||
*/ | ||
public static boolean isNullableComposedSchema(ComposedSchema schema) { | ||
List<Schema> oneOf = schema.getOneOf(); | ||
if (oneOf != null && oneOf.size() <= 2) { | ||
for (Schema s : oneOf) { | ||
if (isNullType(s)) { | ||
return true; | ||
} | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
/** | ||
* isNullType returns true if the input schema is the 'null' type. | ||
* | ||
* The 'null' type is supported in OAS 3.1 and above. It is not supported | ||
* in OAS 2.0 and OAS 3.0.x. | ||
* | ||
* For example, the "null" type could be used to specify that a value must | ||
* either be null or a specified type: | ||
* | ||
* OptionalOrder: | ||
* oneOf: | ||
* - type: 'null' | ||
* - $ref: '#/components/schemas/Order' | ||
*/ | ||
public static boolean isNullType(Schema schema) { | ||
if ("null".equals(schema.getType())) { | ||
return true; | ||
} | ||
return false; | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note to reviewer: this was discussed with @wing328:
If I remember correctly, that refers to InlineModelResolver (https://github.com/OpenAPITools/openapi-generator/blob/master/modules/openapi-generator/src/main/java/org/openapitools/codegen/InlineModelResolver.java) not supporting allOf so the warning notifies the users to use $ref instead of defining the schema inline in allOf . We should have improved the InlineModelResolver to better handle inline schema allOf so I think it's ok to remove the warning.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not aware of the discussion, but I don't know if it's related to InlineModelResolver. This method is only called from here:
openapi-generator/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java
Line 1725 in 0693a83
getSchemaType
will callModelUtils.getInterfaces
on a composed schema, then iterate all the schemas and get the first defined schema as the "type" of the composed schema. I don't think that behavior is correct because it assumes or suggests that composed schemas have some concept of "interfaces".InlineModelResolver has other issues, in that it tries to do a best-guess of models defined inline and resolve them to Schemas defined at the top of the document. This goes against requirements in the spec which claim that inline models should not be considered for composed schemas. At a certain point, we have no information whether our model was initially inline.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is also a non-obvious point: the 'allOf' name is constructed from the first element in the "allOf" list. If two separate authors want to model class inheritance, one OAS author may think it's better to put the "parent" class first, and another author may think it's better to put the "parent" last.