diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/InlineModelResolver.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/InlineModelResolver.java index 703c02474912..4a9a163f8c37 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/InlineModelResolver.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/InlineModelResolver.java @@ -17,6 +17,10 @@ package org.openapitools.codegen; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; import io.swagger.v3.core.util.Json; import io.swagger.v3.oas.models.*; import io.swagger.v3.oas.models.callbacks.Callback; @@ -25,6 +29,7 @@ import io.swagger.v3.oas.models.parameters.RequestBody; import io.swagger.v3.oas.models.responses.ApiResponse; import io.swagger.v3.oas.models.responses.ApiResponses; +import org.apache.commons.lang3.StringUtils; import org.openapitools.codegen.utils.ModelUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -36,6 +41,17 @@ public class InlineModelResolver { private OpenAPI openapi; private Map addedModels = new HashMap(); private Map generatedSignature = new HashMap(); + + // structure mapper sorts properties alphabetically on write to ensure models are + // serialized consistently for lookup of existing models + private static ObjectMapper structureMapper; + + static { + structureMapper = Json.mapper().copy(); + structureMapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true); + structureMapper.writer(new DefaultPrettyPrinter()); + } + static Logger LOGGER = LoggerFactory.getLogger(InlineModelResolver.class); void flatten(OpenAPI openapi) { @@ -488,15 +504,25 @@ private String resolveModelName(String title, String key) { } private String matchGenerated(Schema model) { - String json = Json.pretty(model); - if (generatedSignature.containsKey(json)) { - return generatedSignature.get(json); + try { + String json = structureMapper.writeValueAsString(model); + if (generatedSignature.containsKey(json)) { + return generatedSignature.get(json); + } + } catch (JsonProcessingException e) { + e.printStackTrace(); } + return null; } private void addGenerated(String name, Schema model) { - generatedSignature.put(Json.pretty(model), name); + try { + String json = structureMapper.writeValueAsString(model); + generatedSignature.put(json, name); + } catch (JsonProcessingException e) { + e.printStackTrace(); + } } /** @@ -620,22 +646,45 @@ private Schema modelFromProperty(OpenAPI openAPI, Schema object, String path) { } XML xml = object.getXml(); Map properties = object.getProperties(); + + // NOTE: + // No need to null check setters below. All defaults in the new'd Schema are null, so setting to null would just be a noop. Schema model = new Schema(); - if (object.getType() != null) { - model.setType(object.getType()); - } - if (object.getFormat() != null) { - // Even though the `format` keyword typically applies to primitive types only, - // the JSON schema specification states `format` can be used for any model type instance - // including object types. - model.setFormat(object.getFormat()); - } + model.setType(object.getType()); + + // Even though the `format` keyword typically applies to primitive types only, + // the JSON schema specification states `format` can be used for any model type instance + // including object types. + model.setFormat(object.getFormat()); + model.setDescription(description); model.setExample(example); model.setName(object.getName()); model.setXml(xml); model.setRequired(object.getRequired()); model.setNullable(object.getNullable()); + model.setDiscriminator(object.getDiscriminator()); + model.setWriteOnly(object.getWriteOnly()); + model.setUniqueItems(object.getUniqueItems()); + model.setTitle(object.getTitle()); + model.setReadOnly(object.getReadOnly()); + model.setPattern(object.getPattern()); + model.setNot(object.getNot()); + model.setMinProperties(object.getMinProperties()); + model.setMinLength(object.getMinLength()); + model.setMinItems(object.getMinItems()); + model.setMinimum(object.getMinimum()); + model.setMaxProperties(object.getMaxProperties()); + model.setMaxLength(object.getMaxLength()); + model.setMaxItems(object.getMaxItems()); + model.setMaximum(object.getMaximum()); + model.setExternalDocs(object.getExternalDocs()); + model.setExtensions(object.getExtensions()); + model.setExclusiveMinimum(object.getExclusiveMinimum()); + model.setExclusiveMaximum(object.getExclusiveMaximum()); + model.setExample(object.getExample()); + model.setDeprecated(object.getDeprecated()); + if (properties != null) { flattenProperties(openAPI, properties, path); model.setProperties(properties); diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/InlineModelResolverTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/InlineModelResolverTest.java index 974e21d833e0..e612f985de3c 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/InlineModelResolverTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/InlineModelResolverTest.java @@ -785,6 +785,109 @@ public void objectComposedWithInline() { checkComposedChildren(openAPI, schema.getOneOf(), "oneOf"); } + + @Test + public void inheritanceWithInlineDiscriminator() { + OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/2_0/regression_6905.yaml"); + new InlineModelResolver().flatten(openAPI); + + assertTrue(openAPI.getComponents().getSchemas().get("PartyType") instanceof StringSchema); + assertTrue(openAPI.getComponents().getSchemas().get("CustomerType") instanceof StringSchema); + assertTrue(openAPI.getComponents().getSchemas().get("Entity") instanceof ObjectSchema); + + assertTrue(openAPI.getComponents().getSchemas().get("Party") instanceof ComposedSchema); + assertTrue(openAPI.getComponents().getSchemas().get("Contact") instanceof ComposedSchema); + assertTrue(openAPI.getComponents().getSchemas().get("Customer") instanceof ComposedSchema); + assertTrue(openAPI.getComponents().getSchemas().get("Person") instanceof ComposedSchema); + assertTrue(openAPI.getComponents().getSchemas().get("Organization") instanceof ComposedSchema); + + assertTrue(openAPI.getComponents().getSchemas().get("ApiError") instanceof ObjectSchema); + + assertFalse(openAPI.getComponents().getSchemas().get("Party_allOf") instanceof ComposedSchema); + assertFalse(openAPI.getComponents().getSchemas().get("Contact_allOf") instanceof ComposedSchema); + assertFalse(openAPI.getComponents().getSchemas().get("Customer_allOf") instanceof ComposedSchema); + assertFalse(openAPI.getComponents().getSchemas().get("Person_allOf") instanceof ComposedSchema); + assertFalse(openAPI.getComponents().getSchemas().get("Organization_allOf") instanceof ComposedSchema); + + // Party + ComposedSchema party = (ComposedSchema) openAPI.getComponents().getSchemas().get("Party"); + List partySchemas = party.getAllOf(); + Schema entity = ModelUtils.getReferencedSchema(openAPI, partySchemas.get(0)); + Schema partyAllOf = ModelUtils.getReferencedSchema(openAPI, partySchemas.get(1)); + + assertEquals(partySchemas.get(0).get$ref(), "#/components/schemas/Entity"); + assertEquals(partySchemas.get(1).get$ref(), "#/components/schemas/Party_allOf"); + + assertNull(party.getDiscriminator()); + assertNull(entity.getDiscriminator()); + assertNotNull(partyAllOf.getDiscriminator()); + assertEquals(partyAllOf.getDiscriminator().getPropertyName(), "party_type"); + assertEquals(partyAllOf.getRequired().get(0), "party_type"); + + + // Contact + ComposedSchema contact = (ComposedSchema) openAPI.getComponents().getSchemas().get("Contact"); + Schema contactAllOf = ModelUtils.getReferencedSchema(openAPI, contact.getAllOf().get(1)); + + assertEquals(contact.getExtensions().get("x-discriminator-value"), "contact"); + assertEquals(contact.getAllOf().get(0).get$ref(), "#/components/schemas/Party"); + assertEquals(contact.getAllOf().get(1).get$ref(), "#/components/schemas/Contact_allOf"); + assertNull(contactAllOf.getDiscriminator()); + + + // Customer + ComposedSchema customer = (ComposedSchema) openAPI.getComponents().getSchemas().get("Customer"); + List customerSchemas = customer.getAllOf(); + Schema customerAllOf = ModelUtils.getReferencedSchema(openAPI, customerSchemas.get(1)); + + assertEquals(customerSchemas.get(0).get$ref(), "#/components/schemas/Party"); + assertNull(customer.getDiscriminator()); + assertEquals(customer.getExtensions().get("x-discriminator-value"), "customer"); + + // Discriminators are not defined at this level in the schema doc + assertNull(customerSchemas.get(0).getDiscriminator()); + assertEquals(customerSchemas.get(1).get$ref(), "#/components/schemas/Customer_allOf"); + assertNull(customerSchemas.get(1).getDiscriminator()); + + // Customer -> Party where Customer defines it's own discriminator + assertNotNull(customerAllOf.getDiscriminator()); + assertEquals(customerAllOf.getDiscriminator().getPropertyName(), "customer_type"); + assertEquals(customerAllOf.getRequired().get(0), "customer_type"); + + + // Person + ComposedSchema person = (ComposedSchema) openAPI.getComponents().getSchemas().get("Person"); + List personSchemas = person.getAllOf(); + Schema personAllOf = ModelUtils.getReferencedSchema(openAPI, personSchemas.get(1)); + + // Discriminators are not defined at this level in the schema doc + assertEquals(personSchemas.get(0).get$ref(), "#/components/schemas/Customer"); + assertNull(personSchemas.get(0).getDiscriminator()); + assertEquals(personSchemas.get(1).get$ref(), "#/components/schemas/Person_allOf"); + assertNull(personSchemas.get(1).getDiscriminator()); + + // Person -> Customer -> Party, so discriminator is not at this level + assertNull(person.getDiscriminator()); + assertEquals(person.getExtensions().get("x-discriminator-value"), "person"); + assertNull(personAllOf.getDiscriminator()); + + + // Organization + ComposedSchema organization = (ComposedSchema) openAPI.getComponents().getSchemas().get("Organization"); + List organizationSchemas = organization.getAllOf(); + Schema organizationAllOf = ModelUtils.getReferencedSchema(openAPI, organizationSchemas.get(1)); + + // Discriminators are not defined at this level in the schema doc + assertEquals(organizationSchemas.get(0).get$ref(), "#/components/schemas/Customer"); + assertNull(organizationSchemas.get(0).getDiscriminator()); + assertEquals(organizationSchemas.get(1).get$ref(), "#/components/schemas/Organization_allOf"); + assertNull(organizationSchemas.get(1).getDiscriminator()); + + // Organization -> Customer -> Party, so discriminator is not at this level + assertNull(organizationAllOf.getDiscriminator()); + assertEquals(organization.getExtensions().get("x-discriminator-value"), "organization"); + } + @Test public void arbitraryObjectModelWithArrayInlineWithTitle() { OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_0/inline_model_resolver.yaml"); @@ -888,4 +991,9 @@ public void callbacks() { assertTrue(properties.get("action") instanceof StringSchema); assertTrue(properties.get("data") instanceof StringSchema); } + + @Test + public void regresssion_6905() { + + } } \ No newline at end of file diff --git a/modules/openapi-generator/src/test/resources/2_0/regression_6905.yaml b/modules/openapi-generator/src/test/resources/2_0/regression_6905.yaml new file mode 100644 index 000000000000..5a750ae1dd3c --- /dev/null +++ b/modules/openapi-generator/src/test/resources/2_0/regression_6905.yaml @@ -0,0 +1,172 @@ +swagger: '2.0' + +info: + title: Test Command model generation + description: Test Command model generation + version: 1.0.0 +definitions: + PartyType: + description: type + type: string + enum: + - customer + - contact + + CustomerType: + description: type + type: string + enum: + - person + - organization + + Entity: + type: object + properties: + id: + type: string + readOnly: true + + Party: + allOf: + - $ref: '#/definitions/Entity' + - type: object + discriminator: party_type + required: + - party_type + properties: + party_type: + readOnly: true + $ref: '#/definitions/PartyType' + tax_id_number: + type: string + + Contact: + x-discriminator-value: contact + allOf: + - $ref: '#/definitions/Party' + - type: object + properties: + first_name: + type: string + last_name: + type: string + suffix: + type: string + dob: + type: string + format: date + + Customer: + x-discriminator-value: customer + allOf: + - $ref: '#/definitions/Party' + - type: object + discriminator: customer_type + required: + - customer_type + properties: + customer_type: + readOnly: true + $ref: '#/definitions/CustomerType' + customer_num: + type: string + external_customer_num: + type: string + Person: + x-discriminator-value: person + allOf: + - $ref: '#/definitions/Customer' + - type: object + properties: + first_name: + type: string + last_name: + type: string + + Organization: + x-discriminator-value: organization + allOf: + - $ref: '#/definitions/Customer' + - type: object + required: + - organization_name + properties: + organization_name: + type: string + + ApiError: + type: object + required: + - code + - message + properties: + code: + type: string + readOnly: true + message: + type: string + readOnly: true + +paths: + /customers: + get: + consumes: [] + operationId: queryCustomers + tags: + - Customer + summary: Get customers + responses: + 200: + description: Success + schema: + type: array + items: + $ref: '#/definitions/Customer' + 400: + description: Bad request. + schema: + $ref: '#/definitions/ApiError' + default: + description: Unknown error. + schema: + $ref: '#/definitions/ApiError' + /contacts: + get: + consumes: [] + operationId: queryContacts + tags: + - Contact + summary: Get contact + responses: + 200: + description: Success + schema: + type: array + items: + $ref: '#/definitions/Contact' + 400: + description: Bad request. + schema: + $ref: '#/definitions/ApiError' + default: + description: Unknown error. + schema: + $ref: '#/definitions/ApiError' + /parties: + get: + consumes: [] + responses: + 200: + description: Success + schema: + type: array + items: + $ref: '#/definitions/Party' + 400: + description: Bad request. + schema: + $ref: '#/definitions/ApiError' + default: + description: Unknown error. + schema: + $ref: '#/definitions/ApiError' diff --git a/samples/openapi3/client/petstore/java/jersey2-java8-special-characters/api/openapi.yaml b/samples/openapi3/client/petstore/java/jersey2-java8-special-characters/api/openapi.yaml index 44c6a2aa3c4a..748bc1b78edf 100644 --- a/samples/openapi3/client/petstore/java/jersey2-java8-special-characters/api/openapi.yaml +++ b/samples/openapi3/client/petstore/java/jersey2-java8-special-characters/api/openapi.yaml @@ -46,8 +46,10 @@ components: properties: prop1: type: string + type: object MySchemaName___Characters_allOf: properties: prop2: type: string + type: object