-
-
Notifications
You must be signed in to change notification settings - Fork 7.5k
Improve OneOf handling with new normalizer REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING #23543
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
Open
jpfinne
wants to merge
23
commits into
OpenAPITools:master
Choose a base branch
from
jpfinne:feature/normalizer_REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
bf1dc5f
normalizer REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING
jpfinne 18ac063
Work in progress
jpfinne bbc0fd0
Improvements
jpfinne 9d9cb63
Merge remote-tracking branch 'origin/master' into feature/normalizer_…
jpfinne af8f6c2
Merge master
jpfinne c963666
Fix invalid path
jpfinne 848e606
Improve assertions
jpfinne 73e1f6e
Fix invalid discriminator value
jpfinne 716688e
filename case
jpfinne cef6534
filename case
jpfinne bcf2047
Rollback composed-oneof.yaml
jpfinne 8319043
Improve normalization
jpfinne 53c6f01
Fix building of allOf
jpfinne 18d2f81
Fix hasParent
jpfinne 4f26f25
Fix some cubic findings
jpfinne d3e23bb
Fix some cubic findings
jpfinne 725a0ae
Fix infinite recursion stopping too early
jpfinne c1da8d4
Force build
jpfinne befa7fa
Use getReferencedSchema in search for properties
jpfinne 5d2fab6
Improve hasParent -> isParentReferencedInChild
jpfinne a6e30d4
Cubic suggestions
jpfinne 59b1c26
Add assertions for JsonSubTypes.Types
jpfinne 0facdf7
Clean moved child
jpfinne File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -78,6 +78,10 @@ public class OpenAPINormalizer { | |
| // are removed as most generators cannot handle such case at the moment | ||
| final String REMOVE_ANYOF_ONEOF_AND_KEEP_PROPERTIES_ONLY = "REMOVE_ANYOF_ONEOF_AND_KEEP_PROPERTIES_ONLY"; | ||
|
|
||
| // when set to true, oneOf is removed and is converted into mappings in a discriminator mapping | ||
| final String REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING = "REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING"; | ||
|
|
||
|
|
||
| // when set to true, oneOf/anyOf with either string or enum string as sub schemas will be simplified | ||
| // to just string | ||
| final String SIMPLIFY_ANYOF_STRING_AND_ENUM_STRING = "SIMPLIFY_ANYOF_STRING_AND_ENUM_STRING"; | ||
|
|
@@ -214,6 +218,7 @@ public OpenAPINormalizer(OpenAPI openAPI, Map<String, String> inputRules) { | |
| ruleNames.add(SIMPLIFY_ONEOF_ANYOF_ENUM); | ||
| ruleNames.add(REMOVE_PROPERTIES_FROM_TYPE_OTHER_THAN_OBJECT); | ||
| ruleNames.add(SORT_MODEL_PROPERTIES); | ||
| ruleNames.add(REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING); | ||
|
|
||
| // rules that are default to true | ||
| rules.put(SIMPLIFY_ONEOF_ANYOF, true); | ||
|
|
@@ -639,6 +644,10 @@ protected void normalizeComponentsSchemas() { | |
|
|
||
| // normalize the schemas | ||
| schemas.put(schemaName, normalizeSchema(schema, new HashSet<>())); | ||
|
|
||
| if (getRule(REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING)) { | ||
| ensureInheritanceForDiscriminatorMappings(schema, schemaName); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -1053,6 +1062,8 @@ protected Schema normalizeOneOf(Schema schema, Set<Schema> visitedSchemas) { | |
| // simplify first as the schema may no longer be a oneOf after processing the rule below | ||
| schema = processSimplifyOneOf(schema); | ||
|
|
||
| schema = processReplaceOneOfByMapping(schema); | ||
|
|
||
| // if it's still a oneOf, loop through the sub-schemas | ||
| if (schema.getOneOf() != null) { | ||
| for (int i = 0; i < schema.getOneOf().size(); i++) { | ||
|
|
@@ -1569,6 +1580,245 @@ protected Schema processSimplifyOneOf(Schema schema) { | |
| return schema; | ||
| } | ||
|
|
||
|
|
||
| /** | ||
| * Ensure inheritance is correctly defined for OneOf and Discriminators. | ||
| * | ||
| * For schemas containing oneOf and discriminator.propertyName: | ||
| * <ul> | ||
| * <li>Create the mappings as $refs</li> | ||
| * <li>Remove OneOf</li> | ||
| * </ul> | ||
| */ | ||
| protected Schema processReplaceOneOfByMapping(Schema schema) { | ||
| if (!getRule(REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING) || schema.getOneOf() == null) { | ||
| return schema; | ||
| } | ||
| Discriminator discriminator = schema.getDiscriminator(); | ||
| if (discriminator != null) { | ||
| boolean inlineSchema = isInlineSchema(schema); | ||
| if (inlineSchema) { | ||
| // the For referenced schemas, ensure that there is an allOf with this schema. | ||
| LOGGER.warn("Inline oneOf schema not supported by REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING normalization"); | ||
| return schema; | ||
| } | ||
| if (discriminator.getMapping() == null && discriminator.getPropertyName() != null) { | ||
| List<Schema> oneOfs = schema.getOneOf(); | ||
| if (oneOfs.stream().anyMatch(oneOf -> oneOf.get$ref() == null)) { | ||
| LOGGER.warn("oneOf should only contain $ref for REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING normalization"); | ||
| return schema; | ||
| } | ||
| Map<String, String> mappings = new TreeMap<>(); | ||
| // is the discriminator qttribute qlready in this schema? | ||
| // if yes, it will be deleted in references oneOf to avoid duplicates | ||
| boolean hasProperty = findProperty(schema, discriminator.getPropertyName(), false, new HashSet<>()) != null; | ||
| discriminator.setMapping(mappings); | ||
| for (Schema oneOf : oneOfs) { | ||
| String refSchema = oneOf.get$ref(); | ||
| String name = getDiscriminatorValue(refSchema, discriminator.getPropertyName(), hasProperty, new HashSet<>(List.of(schema))); | ||
| mappings.put(name, refSchema); | ||
|
|
||
| } | ||
| // remove oneOf and only keep the new discriminator mapping | ||
| schema.setOneOf(null); | ||
| } else if (discriminator.getPropertyName() == null) { | ||
| LOGGER.warn("Missing property name in discriminator"); | ||
| } else if (discriminator.getMapping() != null && discriminator.getMapping().size() != schema.getOneOf().size()) { | ||
| LOGGER.warn("Discriminator mapping size " + discriminator.getMapping().size() + " mismatch with oneOf size " + schema.getOneOf().size()); | ||
| } else { | ||
| // remove oneOf and only keep the discriminator mapping | ||
| LOGGER.info("Removing oneOf, discriminator mapping takes precedences on OneOfs"); | ||
| schema.setOneOf(null); | ||
| } | ||
| } | ||
|
|
||
| return schema; | ||
| } | ||
|
|
||
| private boolean isInlineSchema(Schema schema) { | ||
| if (openAPI.getComponents()!=null && openAPI.getComponents().getSchemas()!=null) { | ||
| int identity = System.identityHashCode(schema); | ||
| for (Schema componentSchema: openAPI.getComponents().getSchemas().values()) { | ||
| if (System.identityHashCode(componentSchema) == identity) { | ||
| return false; | ||
| } | ||
| } | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
|
||
| } | ||
| return true; | ||
| } | ||
|
|
||
| /** | ||
| * Best effort to retrieve a good discriminator value. | ||
| * By order of precedence: | ||
| * <ul> | ||
| * <li>x-discriminator-value</li> | ||
| * <li>single enum value for attribute used by the discriminator.propertyName</li> | ||
| * <li>hame of the schema</li> | ||
| * </ul> | ||
| * | ||
| * @param refSchema $ref value like #/components/schemas/Dog | ||
| * @param discriminatorPropertyName name of the property used in the discriminator mapping | ||
| * @param propertyAlreadyPresent if true, delete the property in the referenced schemas to avoid duplicates | ||
| * | ||
| * @return the name | ||
| */ | ||
| protected String getDiscriminatorValue(String refSchema, String discriminatorPropertyName, boolean propertyAlreadyPresent, Set<Schema> visitedSchemas) { | ||
| String schemaName = ModelUtils.getSimpleRef(refSchema); | ||
| Schema schema = ModelUtils.getSchema(openAPI, schemaName); | ||
| Schema property = findProperty(schema, discriminatorPropertyName, propertyAlreadyPresent, visitedSchemas); | ||
| if (schema != null && schema.getExtensions() != null) { | ||
| Object discriminatorValue = schema.getExtensions().get("x-discriminator-value"); | ||
| if (discriminatorValue != null) { | ||
| return discriminatorValue.toString(); | ||
| } | ||
| } | ||
|
|
||
| // find the discriminator value as a unique enum value | ||
| property = ModelUtils.getReferencedSchema(openAPI, property); | ||
| if (property != null) { | ||
| List enums = property.getEnum(); | ||
| if (enums != null && enums.size() == 1) { | ||
| return enums.get(0).toString(); | ||
| } | ||
| } | ||
|
|
||
| return schemaName; | ||
| } | ||
|
|
||
| /** | ||
| * find a property under the schema. | ||
| * | ||
| * @param schema | ||
| * @param propertyName property to find | ||
| * @param toDelete if true delete the found property | ||
| * @param visitedSchemas avoid infinite recursion | ||
| * @return found property or null if not found. | ||
| */ | ||
| private Schema findProperty(Schema schema, String propertyName, boolean toDelete, Set<Schema> visitedSchemas) { | ||
| schema = ModelUtils.getReferencedSchema(openAPI, schema); | ||
| if (propertyName == null || schema == null || visitedSchemas.contains(schema)) { | ||
| return null; | ||
| } | ||
| visitedSchemas.add(schema); | ||
| Map<String, Schema> properties = schema.getProperties(); | ||
| if (properties != null) { | ||
| Schema property = ModelUtils.getReferencedSchema(openAPI, properties.get(propertyName)); | ||
| if (property != null) { | ||
| if (toDelete) { | ||
| if (schema.getProperties().remove(propertyName) != null) { | ||
| LOGGER.info("property " + propertyName + " has been removed in REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING normalization"); | ||
| if (schema.getProperties().isEmpty()) { | ||
| schema.setProperties(null); | ||
| } | ||
| } | ||
| } | ||
| return property; | ||
| } | ||
| } | ||
| List<Schema> allOfs = schema.getAllOf(); | ||
| if (allOfs != null) { | ||
| for (Schema child : allOfs) { | ||
| Schema found = findProperty(child, propertyName, toDelete, visitedSchemas); | ||
| if (found != null) { | ||
| return found; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
|
|
||
| /** | ||
| * ensure that all schemas referenced in the discriminator mapping has an allOf to the parent schema. | ||
| * | ||
| * This allows DefaultCodeGen to detect inheritance. | ||
| * | ||
| * @param parent parent schma | ||
| * @param parentName name of the parent schema | ||
| */ | ||
| protected void ensureInheritanceForDiscriminatorMappings(Schema parent, String parentName) { | ||
| Discriminator discriminator = parent.getDiscriminator(); | ||
| if (discriminator != null && discriminator.getMapping() != null) { | ||
| for (String mapping : discriminator.getMapping().values()) { | ||
| String refSchemaName = ModelUtils.getSimpleRef(mapping); | ||
| Schema child = ModelUtils.getSchema(openAPI, refSchemaName); | ||
| if (child != null) { | ||
| if (parentName != null) { | ||
| ensureInheritanceForDiscriminatorMapping(parent, child, parentName, new HashSet<>()); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * If not already present, add in the child an allOf referencing the parent. | ||
| */ | ||
| protected void ensureInheritanceForDiscriminatorMapping(Schema parent, Schema child, String parentName, Set<Schema> visitedSchemas) { | ||
|
Contributor
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. P2: Renaming this protected helper is a breaking API change for subclasses; downstream overrides of the old method name will no longer apply. Prompt for AI agents |
||
| String reference = "#/components/schemas/" + parentName; | ||
| List<Schema> allOf = child.getAllOf(); | ||
| if (allOf != null) { | ||
| if (isParentReferencedInChild(parent, child, reference, visitedSchemas)) { | ||
| // already done, so no need to add | ||
| return; | ||
| } | ||
| Schema refToParent = new Schema<>().$ref(reference); | ||
| allOf.add(refToParent); | ||
| } else { | ||
| allOf = new ArrayList<>(); | ||
| child.setAllOf(allOf); | ||
| Schema refToParent = new Schema<>().$ref(reference); | ||
| allOf.add(refToParent); | ||
| Map<String, Schema> childProperties = child.getProperties(); | ||
| if (childProperties != null) { | ||
| // move the properties inside the new allOf. | ||
| Schema newChildProperties = new Schema<>() | ||
| .properties(childProperties) | ||
| .additionalProperties(child.getAdditionalProperties()); | ||
| ModelUtils.copyMetadata(child, newChildProperties); | ||
| allOf.add(newChildProperties); | ||
| child.properties(null) | ||
| .type(null) | ||
| .additionalProperties(null) | ||
| .description(null) | ||
| ._default(null) | ||
| .deprecated(null) | ||
| .example(null) | ||
| .examples(null) | ||
| .readOnly(null) | ||
| .writeOnly(null) | ||
| .title(null); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * return true if the child as an allOf referencing the parent schema. | ||
| */ | ||
| private boolean isParentReferencedInChild(Schema parent, Schema child, String reference, Set<Schema> visitedSchemas) { | ||
| if (child == null || visitedSchemas.contains(child)) { | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
|
||
| return false; | ||
| } | ||
| if (child.get$ref() != null && child.get$ref().equals(reference)) { | ||
| return true; | ||
| } | ||
| child = ModelUtils.getReferencedSchema(openAPI, child); | ||
| if (visitedSchemas.contains(child)) { | ||
| return false; | ||
| } | ||
| visitedSchemas.add(child); | ||
| List<Schema> allOf = child.getAllOf(); | ||
| if (allOf != null) { | ||
| for (Schema schema : allOf) { | ||
| if (isParentReferencedInChild(parent, schema, reference, visitedSchemas)) { | ||
| return true; | ||
| } | ||
| } | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * Set nullable to true in array/set if needed. | ||
| * | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
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.
P2:
ensureInheritanceForDiscriminatorMappings()is invoked on the pre-normalization schema instance, so any normalization path that returns a new Schema object makes the discriminator mapping changes apply to a discarded object.Prompt for AI agents