-
-
Notifications
You must be signed in to change notification settings - Fork 7.5k
[Java] [Spring] OpenAPI normalizer USE_UNWRAPPED_FOR_COMPOSITE_ONEOF for mixed OneOf support and jackson JsonUnwrapped #23761
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
base: master
Are you sure you want to change the base?
Changes from 16 commits
2e9ba37
5a4dd70
a6f9281
cd2a1c5
c132be4
59c67f2
a286059
32eb110
d38d951
ceb1af9
536d359
822085e
fd72fd3
784da69
621c3da
2f06df2
e04af45
997b859
5f0bfe1
5ebc7e2
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 |
|---|---|---|
|
|
@@ -159,6 +159,7 @@ public class OpenAPINormalizer { | |
| // when set to true, sort model properties by name to ensure deterministic output | ||
| final String SORT_MODEL_PROPERTIES = "SORT_MODEL_PROPERTIES"; | ||
|
|
||
| final String USE_UNWRAPPED_FOR_INLINE_ONEOF = "USE_UNWRAPPED_FOR_INLINE_ONEOF"; | ||
| // ============= end of rules ============= | ||
|
|
||
| /** | ||
|
|
@@ -219,6 +220,7 @@ public OpenAPINormalizer(OpenAPI openAPI, Map<String, String> inputRules) { | |
| ruleNames.add(REMOVE_PROPERTIES_FROM_TYPE_OTHER_THAN_OBJECT); | ||
| ruleNames.add(SORT_MODEL_PROPERTIES); | ||
| ruleNames.add(REPLACE_ONE_OF_BY_DISCRIMINATOR_MAPPING); | ||
| ruleNames.add(USE_UNWRAPPED_FOR_INLINE_ONEOF); | ||
|
|
||
| // rules that are default to true | ||
| rules.put(SIMPLIFY_ONEOF_ANYOF, true); | ||
|
|
@@ -770,7 +772,7 @@ public Schema normalizeSchema(Schema schema, Set<Schema> visitedSchemas) { | |
| } | ||
|
|
||
| if (ModelUtils.hasAllOf(schema)) { | ||
| return normalizeAllOf(schema, visitedSchemas); | ||
| schema = normalizeAllOf(schema, visitedSchemas); | ||
| } | ||
|
|
||
| if (ModelUtils.hasOneOf(schema)) { | ||
|
|
@@ -1079,6 +1081,8 @@ protected Schema normalizeOneOf(Schema schema, Set<Schema> visitedSchemas) { | |
| schema.getOneOf().set(i, normalizeSchema((Schema) item, visitedSchemas)); | ||
| } | ||
| schema = processReplaceOneOfByMapping(schema); | ||
| schema = processUnwrappedOneOf(schema); | ||
|
|
||
| } else { | ||
| // normalize it as it's no longer an oneOf | ||
| schema = normalizeSchema(schema, visitedSchemas); | ||
|
|
@@ -1087,6 +1091,49 @@ protected Schema normalizeOneOf(Schema schema, Set<Schema> visitedSchemas) { | |
| return schema; | ||
| } | ||
|
|
||
| protected Schema processUnwrappedOneOf(Schema schema) { | ||
| if (!getRule(USE_UNWRAPPED_FOR_INLINE_ONEOF)) { | ||
| return schema; | ||
| } | ||
| if (!(ModelUtils.hasOneOf(schema) && (ModelUtils.hasProperties(schema) || ModelUtils.hasAllOf(schema)))) { | ||
| return schema; | ||
| } | ||
|
|
||
| // skip handling of oneOf + properties + discriminator | ||
| // accept discriminator mappings NOT matching the oneOf elements. | ||
| Discriminator discriminator = schema.getDiscriminator(); | ||
| boolean hasDiscriminator = schema.getDiscriminator() != null; | ||
| if (hasDiscriminator) { | ||
| List<Schema> oneOfs = schema.getOneOf(); | ||
| if (oneOfs.stream().allMatch(oneOf -> oneOf.get$ref() != null)) { | ||
| // skip normalization if discriminator but not maping | ||
| if (discriminator.getMapping() == null && discriminator.getPropertyName() != null) { | ||
| return schema; | ||
| } | ||
| // if same size for mapping and oneOf, assume that we skip this normalization | ||
| if (discriminator.getMapping() != null && discriminator.getMapping().size() == schema.getOneOf().size()) { | ||
| return schema; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Schema newOneOfSchema = new ComposedSchema(); | ||
| newOneOfSchema.oneOf(new ArrayList<>(schema.getOneOf())); | ||
| newOneOfSchema.addExtension(X_ONE_OF_UNWRAPPED, true); | ||
| schema.oneOf(null); | ||
| // TODO: configuration of the property name | ||
| String propertyName = "oneOf"; | ||
|
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: Synthetic Prompt for AI agents |
||
| if (ModelUtils.hasProperties(schema)) { | ||
| schema.getProperties().put(propertyName, newOneOfSchema); | ||
| } else if (ModelUtils.hasAllOf(schema)) { | ||
| Schema allOfSchema = new Schema(); | ||
| allOfSchema.addProperty(propertyName, newOneOfSchema); | ||
| schema.getAllOf().add(allOfSchema); | ||
| } | ||
|
|
||
| return schema; | ||
| } | ||
|
|
||
| protected Schema normalizeAnyOf(Schema schema, Set<Schema> visitedSchemas) { | ||
| //transform anyOf into enums if needed | ||
| schema = processSimplifyAnyOfEnum(schema); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| package {{invokerPackage}}; | ||
|
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. P1: Unsynchronized lazy initialization can overwrite a concurrently supplied custom mapper and make mapper configuration nondeterministic. Prompt for AI agents |
||
| {{#vendorExtensions.x-jacksonMixinConfig}} | ||
|
|
||
| {{^useJackson3}} | ||
| import {{jacksonPackage}}.databind.ObjectMapper; | ||
| {{/useJackson3}} | ||
| import {{jacksonPackage}}.databind.json.JsonMapper; | ||
| {{#mixins}} | ||
| import {{modelPackage}}.{{.}}; | ||
| {{/mixins}} | ||
|
|
||
| public class JacksonMixinConfig { | ||
| private static volatile {{mapper}} INSTANCE; | ||
|
|
||
| /** | ||
| Get the {{mapper}} used by the @JsonCreator in @JsonUnWrapped interfaces. | ||
| */ | ||
| public static {{mapper}} getMapper() { | ||
| if (INSTANCE == null) { | ||
| setBuilder({{^useJackson3}}JsonMapper.builder().findAndAddModules(){{/useJackson3}}{{#useJackson3}}JsonMapper.shared().rebuild(){{/useJackson3}}); | ||
| } | ||
| return INSTANCE; | ||
| } | ||
|
|
||
| /** | ||
| Initialize the mapper used by the @JsonCreator. | ||
| <p> | ||
| Do not pass the global {{mapper}} as @JsonUnwrapped does not support deserializer. | ||
| */ | ||
| public static void set{{mapper}}({{mapper}} mapper) { | ||
| INSTANCE = mapper; | ||
| } | ||
|
|
||
| /** | ||
| Initialize the mapper used by the @JsonCreator. | ||
| <p> | ||
| Configure the Mixins | ||
| */ | ||
| public static void setBuilder(JsonMapper.Builder jsonMapperBuilder) { | ||
| INSTANCE = jsonMapperBuilder{{#mixins}} | ||
| .addMixIn({{.}}.class, {{.}}.{{.}}Mixin.class){{/mixins}} | ||
| .build(); | ||
| } | ||
| {{/vendorExtensions.x-jacksonMixinConfig}} | ||
|
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. P1: Class closing brace is outside the conditional Mustache section, producing invalid Java if the section is omitted Prompt for AI agents |
||
| } | ||
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.
P1: NullPointerException in processUnwrapCompositeOneOf when oneOf list contains null elements. normalizeOneOf explicitly tolerates null entries (
if (item == null) continue;) but processUnwrapCompositeOneOf's stream allMatch dereferences elements without null checking (oneOf.get$ref()), causing an NPE.Prompt for AI agents