diff --git a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/util/ResolverFully.java b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/util/ResolverFully.java index 755e8b6d32..376c926852 100644 --- a/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/util/ResolverFully.java +++ b/modules/swagger-parser-v3/src/main/java/io/swagger/v3/parser/util/ResolverFully.java @@ -396,7 +396,8 @@ private Schema resolveSchemaImpl(Schema schema) { } } - if(schema instanceof ArraySchema) { + // These subclass checks cover OAS 3.0 deserialization; OAS 3.1 uses JsonSchema. + if (schema instanceof ArraySchema) { ArraySchema arrayModel = (ArraySchema) schema; arrayModel.setItems(resolveSchema(arrayModel.getItems())); return arrayModel; @@ -426,39 +427,48 @@ private Schema resolveSchemaImpl(Schema schema) { return obj; } + if (SpecVersion.V31.equals(schema.getSpecVersion())) { + // OAS 3.1 schemas can combine keywords, so resolve these fields without returning early. + resolveOas31SchemaFields(schema); + } + + if (schema.getNot() != null) { + schema.setNot(resolveSchema(schema.getNot())); + } Schema result = schema; - if (schema instanceof ComposedSchema) { - ComposedSchema composedSchema = (ComposedSchema) schema; + boolean hasAllOf = schema.getAllOf() != null; + boolean hasAnyOf = schema.getAnyOf() != null; + boolean hasOneOf = schema.getOneOf() != null; - boolean hasAllOf = composedSchema.getAllOf() != null; - boolean hasAnyOf = composedSchema.getAnyOf() != null; - boolean hasOneOf = composedSchema.getOneOf() != null; + if (hasAllOf || hasAnyOf || hasOneOf) { boolean adjacent = (hasAllOf && hasAnyOf) || (hasAllOf && hasOneOf) || (hasAnyOf && hasOneOf); if (aggregateCombinators && (hasAllOf || adjacent)) { + // TODO: Aggregating into a new schema can drop sibling keywords from the source OAS 3.1 + // schema (for example, items); preserve them by intersecting the source and aggregated schemas. + // For example: schema: {items: {type: string}, allOf: [{type: object}]}. Schema combinedModel = null; - if (SpecVersion.V30.equals(composedSchema.getSpecVersion())) { - combinedModel = SchemaTypeUtil.createSchema(getSchemaType(composedSchema), composedSchema.getFormat()); + if (SpecVersion.V30.equals(schema.getSpecVersion())) { + combinedModel = SchemaTypeUtil.createSchema(getSchemaType(schema), schema.getFormat()); } else { combinedModel = new JsonSchema(); - combinedModel.setFormat(composedSchema.getFormat()); - combinedModel.setTypes(composedSchema.getTypes()); + combinedModel.setFormat(schema.getFormat()); + combinedModel.setTypes(schema.getTypes()); } -// combinedModel.setDefault(composedSchema.getDefault()); Set examples = new HashSet<>(); Set defaultValues = new HashSet<>(); if (hasAllOf) { - aggregateSchemaCombinators(composedSchema, combinedModel, composedSchema.getAllOf(), examples, defaultValues); + aggregateSchemaCombinators(schema, combinedModel, schema.getAllOf(), examples, defaultValues); } if (hasOneOf) { - aggregateSchemaCombinators(composedSchema, combinedModel, composedSchema.getOneOf(), examples, defaultValues); + aggregateSchemaCombinators(schema, combinedModel, schema.getOneOf(), examples, defaultValues); } if (hasAnyOf) { - aggregateSchemaCombinators(composedSchema, combinedModel, composedSchema.getAnyOf(), examples, defaultValues); + aggregateSchemaCombinators(schema, combinedModel, schema.getAnyOf(), examples, defaultValues); } if (defaultValues.size() == 1) { Object defaultValue = defaultValues.iterator().next(); @@ -506,13 +516,16 @@ private Schema resolveSchemaImpl(Schema schema) { } else { // User doesn't need or want to aggregate composed schema, we only solve refs if (hasAllOf) { - composedSchema.allOf(composedSchema.getAllOf().stream().map(this::resolveSchema).collect(Collectors.toList())); + List allOf = schema.getAllOf(); + schema.allOf(allOf.stream().map(this::resolveSchema).collect(Collectors.toList())); } if (hasOneOf) { - composedSchema.oneOf(composedSchema.getOneOf().stream().map(this::resolveSchema).collect(Collectors.toList())); + List oneOf = schema.getOneOf(); + schema.oneOf(oneOf.stream().map(this::resolveSchema).collect(Collectors.toList())); } if (hasAnyOf) { - composedSchema.anyOf(composedSchema.getAnyOf().stream().map(this::resolveSchema).collect(Collectors.toList())); + List anyOf = schema.getAnyOf(); + schema.anyOf(anyOf.stream().map(this::resolveSchema).collect(Collectors.toList())); } } } @@ -551,6 +564,63 @@ private Schema resolveSchemaImpl(Schema schema) { return result; } + private void resolveOas31SchemaFields(Schema schema) { + if (schema.getItems() != null) { + schema.setItems(resolveSchema(schema.getItems())); + } + if (schema.getPrefixItems() != null) { + List prefixItems = schema.getPrefixItems(); + schema.setPrefixItems(prefixItems.stream().map(this::resolveSchema).collect(Collectors.toList())); + } + if (schema.getUnevaluatedItems() != null) { + schema.setUnevaluatedItems(resolveSchema(schema.getUnevaluatedItems())); + } + // additionalItems is retained as an extension by OpenAPIDeserializer. + + if (schema.getAdditionalProperties() instanceof Schema) { + schema.setAdditionalProperties(resolveSchema((Schema) schema.getAdditionalProperties())); + } + if (schema.getPatternProperties() != null) { + Map patternProperties = schema.getPatternProperties(); + Map resolved = new LinkedHashMap<>(); + for (Map.Entry entry : patternProperties.entrySet()) { + resolved.put(entry.getKey(), resolveSchema(entry.getValue())); + } + schema.setPatternProperties(resolved); + } + if (schema.getPropertyNames() != null) { + schema.setPropertyNames(resolveSchema(schema.getPropertyNames())); + } + if (schema.getUnevaluatedProperties() != null) { + schema.setUnevaluatedProperties(resolveSchema(schema.getUnevaluatedProperties())); + } + + if (schema.getIf() != null) { + schema.setIf(resolveSchema(schema.getIf())); + } + if (schema.getThen() != null) { + schema.setThen(resolveSchema(schema.getThen())); + } + if (schema.getElse() != null) { + schema.setElse(resolveSchema(schema.getElse())); + } + + if (schema.getContains() != null) { + schema.setContains(resolveSchema(schema.getContains())); + } + if (schema.getContentSchema() != null) { + schema.setContentSchema(resolveSchema(schema.getContentSchema())); + } + if (schema.getDependentSchemas() != null) { + Map dependentSchemas = schema.getDependentSchemas(); + Map resolved = new LinkedHashMap<>(); + for (Map.Entry entry : dependentSchemas.entrySet()) { + resolved.put(entry.getKey(), resolveSchema(entry.getValue())); + } + schema.setDependentSchemas(resolved); + } + } + protected String getSchemaType(Schema schema) { if (SpecVersion.V30.equals(schema.getSpecVersion())) { return schema.getType(); @@ -587,7 +657,7 @@ public Map resolveExample(Map examples){ } - private void aggregateSchemaCombinators(ComposedSchema sourceSchema, Schema targetSchema, + private void aggregateSchemaCombinators(Schema sourceSchema, Schema targetSchema, List schemasToAggregate, Set examples, Set defaultValues) { Set requiredProperties = new HashSet<>(); diff --git a/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/ResolveCombinatorsOas31Test.java b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/ResolveCombinatorsOas31Test.java new file mode 100644 index 0000000000..0cb61d8ca3 --- /dev/null +++ b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/ResolveCombinatorsOas31Test.java @@ -0,0 +1,576 @@ +package io.swagger.v3.parser.test; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.SpecVersion; +import io.swagger.v3.oas.models.media.JsonSchema; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.parser.OpenAPIV3Parser; +import io.swagger.v3.parser.core.models.ParseOptions; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.testng.annotations.Test; + +import static org.testng.Assert.*; + +public class ResolveCombinatorsOas31Test { + + private ParseOptions resolveCombinatorsOptions() { + ParseOptions options = new ParseOptions(); + options.setResolve(true); + options.setResolveFully(true); + options.setResolveCombinators(true); + return options; + } + + private OpenAPI parse(String yaml) { + return parse(yaml, resolveCombinatorsOptions()); + } + + private OpenAPI parse(String yaml, ParseOptions options) { + OpenAPI openAPI = new OpenAPIV3Parser().readContents(yaml, null, options).getOpenAPI(); + assertNotNull(openAPI); + return openAPI; + } + + private Schema requestBodySchema(OpenAPI openAPI) { + return openAPI.getPaths().get("/example").getPost() + .getRequestBody().getContent().get("application/json").getSchema(); + } + + private Schema responseSchema(OpenAPI openAPI) { + return openAPI.getPaths().get("/example").getGet() + .getResponses().get("200").getContent().get("application/json").getSchema(); + } + + @Test + public void allOfMergesForOas30() { + String yaml = "openapi: 3.0.0\n" + + "info:\n" + + " title: Test\n" + + " version: 1.0.0\n" + + "paths:\n" + + " /example:\n" + + " post:\n" + + " requestBody:\n" + + " content:\n" + + " application/json:\n" + + " schema:\n" + + " type: object\n" + + " allOf:\n" + + " - type: object\n" + + " properties:\n" + + " a:\n" + + " type: string\n" + + " - type: object\n" + + " properties:\n" + + " b:\n" + + " type: string\n" + + " responses:\n" + + " '200':\n" + + " description: OK\n"; + + OpenAPI openAPI = parse(yaml); + + Schema combined = requestBodySchema(openAPI); + assertNull(combined.getAllOf(), "allOf should be merged away for OAS 3.0"); + assertNotNull(combined.getProperties()); + assertTrue(combined.getProperties().containsKey("a")); + assertTrue(combined.getProperties().containsKey("b")); + } + + @Test + public void allOfMergesForOas31() { + String yaml = "openapi: 3.1.0\n" + + "info:\n" + + " title: Test\n" + + " version: 1.0.0\n" + + "paths:\n" + + " /example:\n" + + " post:\n" + + " requestBody:\n" + + " content:\n" + + " application/json:\n" + + " schema:\n" + + " type: object\n" + + " allOf:\n" + + " - type: object\n" + + " properties:\n" + + " a:\n" + + " type: string\n" + + " - type: object\n" + + " properties:\n" + + " b:\n" + + " type: string\n" + + " responses:\n" + + " '200':\n" + + " description: OK\n"; + + OpenAPI openAPI = parse(yaml); + + Schema combined = requestBodySchema(openAPI); + assertNull(combined.getAllOf(), "allOf should be merged away for OAS 3.1, same as OAS 3.0"); + assertNotNull(combined.getProperties()); + assertTrue(combined.getProperties().containsKey("a")); + assertTrue(combined.getProperties().containsKey("b")); + } + + @Test + public void responseSchemaAllOfMergesForOas31() { + String yaml = "openapi: 3.1.0\n" + + "info:\n" + + " title: Test\n" + + " version: 1.0.0\n" + + "paths:\n" + + " /example:\n" + + " get:\n" + + " responses:\n" + + " '200':\n" + + " description: OK\n" + + " content:\n" + + " application/json:\n" + + " schema:\n" + + " type: object\n" + + " allOf:\n" + + " - type: object\n" + + " properties:\n" + + " id:\n" + + " type: string\n" + + " - type: object\n" + + " properties:\n" + + " name:\n" + + " type: string\n"; + + Schema combined = responseSchema(parse(yaml)); + assertNull(combined.getAllOf(), "response allOf should be merged for OAS 3.1"); + assertNotNull(combined.getProperties()); + assertTrue(combined.getProperties().containsKey("id")); + assertTrue(combined.getProperties().containsKey("name")); + } + + // Adjacent combinators are intentionally aggregated here to match existing OAS 3.0 ResolverFully behavior. + @Test + public void oneOfMergesForOas31WhenAdjacentToAllOf() { + String yaml = "openapi: 3.1.0\n" + + "info:\n" + + " title: Test\n" + + " version: 1.0.0\n" + + "paths:\n" + + " /example:\n" + + " post:\n" + + " requestBody:\n" + + " content:\n" + + " application/json:\n" + + " schema:\n" + + " type: object\n" + + " allOf:\n" + + " - type: object\n" + + " properties:\n" + + " a:\n" + + " type: string\n" + + " oneOf:\n" + + " - type: object\n" + + " properties:\n" + + " b:\n" + + " type: string\n" + + " - type: object\n" + + " properties:\n" + + " c:\n" + + " type: string\n" + + " responses:\n" + + " '200':\n" + + " description: OK\n"; + + OpenAPI openAPI = parse(yaml); + + Schema combined = requestBodySchema(openAPI); + assertNull(combined.getAllOf(), "allOf adjacent to oneOf should be merged for OAS 3.1"); + assertNull(combined.getOneOf(), "oneOf adjacent to allOf should be merged for OAS 3.1"); + assertNotNull(combined.getProperties()); + assertTrue(combined.getProperties().containsKey("a")); + assertTrue(combined.getProperties().containsKey("b")); + assertTrue(combined.getProperties().containsKey("c")); + } + + @Test + public void anyOfMergesForOas31WhenAdjacentToAllOf() { + String yaml = "openapi: 3.1.0\n" + + "info:\n" + + " title: Test\n" + + " version: 1.0.0\n" + + "paths:\n" + + " /example:\n" + + " post:\n" + + " requestBody:\n" + + " content:\n" + + " application/json:\n" + + " schema:\n" + + " type: object\n" + + " allOf:\n" + + " - type: object\n" + + " properties:\n" + + " a:\n" + + " type: string\n" + + " anyOf:\n" + + " - type: object\n" + + " properties:\n" + + " b:\n" + + " type: string\n" + + " - type: object\n" + + " properties:\n" + + " c:\n" + + " type: string\n" + + " responses:\n" + + " '200':\n" + + " description: OK\n"; + + OpenAPI openAPI = parse(yaml); + + Schema combined = requestBodySchema(openAPI); + assertNull(combined.getAllOf(), "allOf adjacent to anyOf should be merged for OAS 3.1"); + assertNull(combined.getAnyOf(), "anyOf adjacent to allOf should be merged for OAS 3.1"); + assertNotNull(combined.getProperties()); + assertTrue(combined.getProperties().containsKey("a")); + assertTrue(combined.getProperties().containsKey("b")); + assertTrue(combined.getProperties().containsKey("c")); + } + + @Test + public void refsInsideOas31AllOfAreDereferencedEvenWhenNotAggregated() { + String yaml = "openapi: 3.1.0\n" + + "info:\n" + + " title: Test\n" + + " version: 1.0.0\n" + + "paths:\n" + + " /example:\n" + + " post:\n" + + " requestBody:\n" + + " content:\n" + + " application/json:\n" + + " schema:\n" + + " type: object\n" + + " allOf:\n" + + " - $ref: '#/components/schemas/SchemaA'\n" + + " - $ref: '#/components/schemas/SchemaB'\n" + + " responses:\n" + + " '200':\n" + + " description: OK\n" + + "components:\n" + + " schemas:\n" + + " SchemaA:\n" + + " type: object\n" + + " properties:\n" + + " a:\n" + + " type: string\n" + + " SchemaB:\n" + + " type: object\n" + + " properties:\n" + + " b:\n" + + " type: string\n"; + + ParseOptions options = new ParseOptions(); + options.setResolve(true); + options.setResolveFully(true); + options.setResolveCombinators(false); + + OpenAPI openAPI = parse(yaml, options); + + Schema combined = requestBodySchema(openAPI); + assertNotNull(combined.getAllOf(), "allOf should be left in place when combinator aggregation is disabled"); + assertEquals(combined.getAllOf().size(), 2); + + Schema branchA = (Schema) combined.getAllOf().get(0); + Schema branchB = (Schema) combined.getAllOf().get(1); + assertNull(branchA.get$ref(), "allOf branch should be dereferenced, not left as a $ref"); + assertNull(branchB.get$ref(), "allOf branch should be dereferenced, not left as a $ref"); + assertNotNull(branchA.getProperties()); + assertTrue(branchA.getProperties().containsKey("a")); + assertNotNull(branchB.getProperties()); + assertTrue(branchB.getProperties().containsKey("b")); + } + + @Test + public void oneOfAndAnyOfMergeForOas31WhenAdjacentWithoutAllOf() { + // "adjacent" combinators trigger merging even without allOf being present at all. + String yaml = "openapi: 3.1.0\n" + + "info:\n" + + " title: Test\n" + + " version: 1.0.0\n" + + "paths:\n" + + " /example:\n" + + " post:\n" + + " requestBody:\n" + + " content:\n" + + " application/json:\n" + + " schema:\n" + + " type: object\n" + + " oneOf:\n" + + " - type: object\n" + + " properties:\n" + + " a:\n" + + " type: string\n" + + " anyOf:\n" + + " - type: object\n" + + " properties:\n" + + " b:\n" + + " type: string\n" + + " responses:\n" + + " '200':\n" + + " description: OK\n"; + + OpenAPI openAPI = parse(yaml); + + Schema combined = requestBodySchema(openAPI); + assertNull(combined.getOneOf(), "oneOf adjacent to anyOf (no allOf) should be merged for OAS 3.1"); + assertNull(combined.getAnyOf(), "anyOf adjacent to oneOf (no allOf) should be merged for OAS 3.1"); + assertNotNull(combined.getProperties()); + assertTrue(combined.getProperties().containsKey("a")); + assertTrue(combined.getProperties().containsKey("b")); + } + + @Test + public void allThreeCombinatorsMergeForOas31() { + String yaml = "openapi: 3.1.0\n" + + "info:\n" + + " title: Test\n" + + " version: 1.0.0\n" + + "paths:\n" + + " /example:\n" + + " post:\n" + + " requestBody:\n" + + " content:\n" + + " application/json:\n" + + " schema:\n" + + " type: object\n" + + " allOf:\n" + + " - type: object\n" + + " properties:\n" + + " a:\n" + + " type: string\n" + + " oneOf:\n" + + " - type: object\n" + + " properties:\n" + + " b:\n" + + " type: string\n" + + " anyOf:\n" + + " - type: object\n" + + " properties:\n" + + " c:\n" + + " type: string\n" + + " responses:\n" + + " '200':\n" + + " description: OK\n"; + + OpenAPI openAPI = parse(yaml); + + Schema combined = requestBodySchema(openAPI); + assertNull(combined.getAllOf()); + assertNull(combined.getOneOf()); + assertNull(combined.getAnyOf()); + assertNotNull(combined.getProperties()); + assertTrue(combined.getProperties().containsKey("a")); + assertTrue(combined.getProperties().containsKey("b")); + assertTrue(combined.getProperties().containsKey("c")); + } + + @Test + public void standaloneOneOfDoesNotMergeForOas31() { + // A lone oneOf (no allOf, no anyOf) is not "adjacent" and should be left as-is, + // matching OAS 3.0 behavior (a discriminated union should not be flattened away). + String yaml = "openapi: 3.1.0\n" + + "info:\n" + + " title: Test\n" + + " version: 1.0.0\n" + + "paths:\n" + + " /example:\n" + + " post:\n" + + " requestBody:\n" + + " content:\n" + + " application/json:\n" + + " schema:\n" + + " type: object\n" + + " oneOf:\n" + + " - type: object\n" + + " properties:\n" + + " a:\n" + + " type: string\n" + + " - type: object\n" + + " properties:\n" + + " b:\n" + + " type: string\n" + + " responses:\n" + + " '200':\n" + + " description: OK\n"; + + OpenAPI openAPI = parse(yaml); + + Schema combined = requestBodySchema(openAPI); + assertNotNull(combined.getOneOf(), "standalone oneOf should not be merged for OAS 3.1, same as OAS 3.0"); + assertEquals(combined.getOneOf().size(), 2); + } + + @Test + public void standaloneAnyOfDoesNotMergeForOas31() { + String yaml = "openapi: 3.1.0\n" + + "info:\n" + + " title: Test\n" + + " version: 1.0.0\n" + + "paths:\n" + + " /example:\n" + + " post:\n" + + " requestBody:\n" + + " content:\n" + + " application/json:\n" + + " schema:\n" + + " type: object\n" + + " anyOf:\n" + + " - type: object\n" + + " properties:\n" + + " a:\n" + + " type: string\n" + + " - type: object\n" + + " properties:\n" + + " b:\n" + + " type: string\n" + + " responses:\n" + + " '200':\n" + + " description: OK\n"; + + OpenAPI openAPI = parse(yaml); + + Schema combined = requestBodySchema(openAPI); + assertNotNull(combined.getAnyOf(), "standalone anyOf should not be merged for OAS 3.1, same as OAS 3.0"); + assertEquals(combined.getAnyOf().size(), 2); + } + + @Test + public void nestedAllOfInsideAllOfBranchMergesRecursivelyForOas31() { + // A branch of the outer allOf itself has a nested allOf - aggregateSchemaCombinators + // recurses to flatten it too, bottom-up. + String yaml = "openapi: 3.1.0\n" + + "info:\n" + + " title: Test\n" + + " version: 1.0.0\n" + + "paths:\n" + + " /example:\n" + + " post:\n" + + " requestBody:\n" + + " content:\n" + + " application/json:\n" + + " schema:\n" + + " type: object\n" + + " allOf:\n" + + " - type: object\n" + + " properties:\n" + + " a:\n" + + " type: string\n" + + " - type: object\n" + + " allOf:\n" + + " - type: object\n" + + " properties:\n" + + " b:\n" + + " type: string\n" + + " - type: object\n" + + " properties:\n" + + " c:\n" + + " type: string\n" + + " responses:\n" + + " '200':\n" + + " description: OK\n"; + + OpenAPI openAPI = parse(yaml); + + Schema combined = requestBodySchema(openAPI); + assertNull(combined.getAllOf(), "nested allOf should be merged away recursively for OAS 3.1"); + assertNotNull(combined.getProperties()); + assertTrue(combined.getProperties().containsKey("a")); + assertTrue(combined.getProperties().containsKey("b")); + assertTrue(combined.getProperties().containsKey("c")); + } + + @Test + public void allOfMergeForOas31AggregatesRequiredDefaultAndExtensions() { + String yaml = "openapi: 3.1.0\n" + + "info:\n" + + " title: Test\n" + + " version: 1.0.0\n" + + "paths:\n" + + " /example:\n" + + " post:\n" + + " requestBody:\n" + + " content:\n" + + " application/json:\n" + + " schema:\n" + + " type: object\n" + + " x-outer: outer-value\n" + + " allOf:\n" + + " - type: object\n" + + " required:\n" + + " - a\n" + + " properties:\n" + + " a:\n" + + " type: string\n" + + " default:\n" + + " a: default-a\n" + + " x-inner: inner-value\n" + + " - type: object\n" + + " required:\n" + + " - b\n" + + " properties:\n" + + " b:\n" + + " type: string\n" + + " responses:\n" + + " '200':\n" + + " description: OK\n"; + + OpenAPI openAPI = parse(yaml); + + Schema combined = requestBodySchema(openAPI); + assertNull(combined.getAllOf()); + assertNotNull(combined.getRequired(), "required from both allOf branches should be aggregated"); + assertTrue(combined.getRequired().contains("a")); + assertTrue(combined.getRequired().contains("b")); + assertTrue(combined.getDefault() instanceof ObjectNode, + "the object default should be represented as a JSON object"); + assertEquals(((ObjectNode) combined.getDefault()).get("a").asText(), "default-a"); + assertNotNull(combined.getExtensions(), "extensions from branches and the source schema should be aggregated"); + assertTrue(combined.getExtensions().containsKey("x-inner")); + assertTrue(combined.getExtensions().containsKey("x-outer")); + } + + @Test + public void allOfMergeForOas31ProducesJsonSchemaWithTypesSet() { + // The merged model construction branches on SpecVersion: OAS 3.1 should produce a + // JsonSchema with the "types" set populated, mirroring OAS 3.0's singular "type" branch. + String yaml = "openapi: 3.1.0\n" + + "info:\n" + + " title: Test\n" + + " version: 1.0.0\n" + + "paths:\n" + + " /example:\n" + + " post:\n" + + " requestBody:\n" + + " content:\n" + + " application/json:\n" + + " schema:\n" + + " type: object\n" + + " allOf:\n" + + " - type: object\n" + + " properties:\n" + + " a:\n" + + " type: string\n" + + " - type: object\n" + + " properties:\n" + + " b:\n" + + " type: string\n" + + " responses:\n" + + " '200':\n" + + " description: OK\n"; + + OpenAPI openAPI = parse(yaml); + + Schema combined = requestBodySchema(openAPI); + assertTrue(combined instanceof JsonSchema, "merged OAS 3.1 schema should be a JsonSchema"); + assertEquals(combined.getSpecVersion(), SpecVersion.V31); + assertNotNull(combined.getTypes(), "merged OAS 3.1 schema should populate the types set, not the singular type field"); + assertTrue(combined.getTypes().contains("object")); + } +} diff --git a/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/ResolveItemsAndAdditionalPropertiesOas31Test.java b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/ResolveItemsAndAdditionalPropertiesOas31Test.java new file mode 100644 index 0000000000..d5517bb7a3 --- /dev/null +++ b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/ResolveItemsAndAdditionalPropertiesOas31Test.java @@ -0,0 +1,239 @@ +package io.swagger.v3.parser.test; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.parser.OpenAPIV3Parser; +import io.swagger.v3.parser.core.models.ParseOptions; +import org.testng.annotations.Test; + +import static org.testng.Assert.*; + +public class ResolveItemsAndAdditionalPropertiesOas31Test { + + private ParseOptions resolveCombinatorsOptions() { + ParseOptions options = new ParseOptions(); + options.setResolve(true); + options.setResolveFully(true); + options.setResolveCombinators(true); + return options; + } + + private Schema requestBodySchema(OpenAPI openAPI) { + return openAPI.getPaths().get("/example").getPost() + .getRequestBody().getContent().get("application/json").getSchema(); + } + + @Test + public void arrayItemsWithAllOfMergeForOas31() { + // The outer schema has no combinator; its nested `items` schema does. OAS 3.1 schemas + // must therefore recurse into `items` instead of relying only on shape-specific subclasses. + String yaml = "openapi: 3.1.0\n" + + "info:\n" + + " title: Test\n" + + " version: 1.0.0\n" + + "paths:\n" + + " /example:\n" + + " post:\n" + + " requestBody:\n" + + " content:\n" + + " application/json:\n" + + " schema:\n" + + " type: array\n" + + " items:\n" + + " type: object\n" + + " allOf:\n" + + " - type: object\n" + + " properties:\n" + + " a:\n" + + " type: string\n" + + " - type: object\n" + + " properties:\n" + + " b:\n" + + " type: string\n" + + " responses:\n" + + " '200':\n" + + " description: OK\n"; + + OpenAPI openAPI = new OpenAPIV3Parser().readContents(yaml, null, resolveCombinatorsOptions()).getOpenAPI(); + assertNotNull(openAPI); + + Schema arraySchema = requestBodySchema(openAPI); + Schema items = arraySchema.getItems(); + assertNotNull(items, "array items should be resolved for OAS 3.1"); + assertNull(items.getAllOf(), "allOf inside array items should be merged for OAS 3.1"); + assertNotNull(items.getProperties()); + assertTrue(items.getProperties().containsKey("a")); + assertTrue(items.getProperties().containsKey("b")); + } + + @Test + public void arrayItemsWithRefResolveForOas31() { + String yaml = "openapi: 3.1.0\n" + + "info:\n" + + " title: Test\n" + + " version: 1.0.0\n" + + "paths:\n" + + " /example:\n" + + " post:\n" + + " requestBody:\n" + + " content:\n" + + " application/json:\n" + + " schema:\n" + + " type: array\n" + + " items:\n" + + " $ref: '#/components/schemas/Item'\n" + + " responses:\n" + + " '200':\n" + + " description: OK\n" + + "components:\n" + + " schemas:\n" + + " Item:\n" + + " type: object\n" + + " properties:\n" + + " a:\n" + + " type: string\n"; + + OpenAPI openAPI = new OpenAPIV3Parser().readContents(yaml, null, resolveCombinatorsOptions()).getOpenAPI(); + assertNotNull(openAPI); + + Schema arraySchema = requestBodySchema(openAPI); + Schema items = arraySchema.getItems(); + assertNotNull(items); + assertNull(items.get$ref(), "array items $ref should be resolved for OAS 3.1"); + assertNotNull(items.getProperties()); + assertTrue(items.getProperties().containsKey("a")); + } + + @Test + public void arrayAndObjectKeywordsAreBothResolvedForOas31() { + // OAS 3.1 permits array- and object-specific keywords on the same schema. With no `type`, + // this schema can describe either an array or an object. Resolving `items` must not return + // early and skip `properties`. + String yaml = "openapi: 3.1.0\n" + + "info:\n" + + " title: Test\n" + + " version: 1.0.0\n" + + "paths:\n" + + " /example:\n" + + " post:\n" + + " requestBody:\n" + + " content:\n" + + " application/json:\n" + + " schema:\n" + + " type:\n" + + " - array\n" + + " - object\n" + + " items:\n" + + " type: string\n" + + " properties:\n" + + " a:\n" + + " type: object\n" + + " allOf:\n" + + " - type: object\n" + + " properties:\n" + + " p:\n" + + " type: string\n" + + " - type: object\n" + + " properties:\n" + + " q:\n" + + " type: string\n" + + " responses:\n" + + " '200':\n" + + " description: OK\n"; + + OpenAPI openAPI = new OpenAPIV3Parser().readContents(yaml, null, resolveCombinatorsOptions()).getOpenAPI(); + assertNotNull(openAPI); + + Schema schema = requestBodySchema(openAPI); + assertNotNull(schema.getItems(), "items should still be resolved"); + assertNotNull(schema.getProperties(), "properties should also be resolved, not skipped"); + + Schema a = (Schema) schema.getProperties().get("a"); + assertNotNull(a); + assertNull(a.getAllOf(), "allOf on a property sibling to items should still be merged for OAS 3.1"); + assertNotNull(a.getProperties()); + assertTrue(a.getProperties().containsKey("p")); + assertTrue(a.getProperties().containsKey("q")); + } + + @Test + public void additionalPropertiesWithAllOfMergeForOas31() { + String yaml = "openapi: 3.1.0\n" + + "info:\n" + + " title: Test\n" + + " version: 1.0.0\n" + + "paths:\n" + + " /example:\n" + + " post:\n" + + " requestBody:\n" + + " content:\n" + + " application/json:\n" + + " schema:\n" + + " type: object\n" + + " additionalProperties:\n" + + " type: object\n" + + " allOf:\n" + + " - type: object\n" + + " properties:\n" + + " a:\n" + + " type: string\n" + + " - type: object\n" + + " properties:\n" + + " b:\n" + + " type: string\n" + + " responses:\n" + + " '200':\n" + + " description: OK\n"; + + OpenAPI openAPI = new OpenAPIV3Parser().readContents(yaml, null, resolveCombinatorsOptions()).getOpenAPI(); + assertNotNull(openAPI); + + Schema mapSchema = requestBodySchema(openAPI); + Object additionalProperties = mapSchema.getAdditionalProperties(); + assertTrue(additionalProperties instanceof Schema, "additionalProperties should be resolved for OAS 3.1"); + Schema additionalPropertiesSchema = (Schema) additionalProperties; + assertNull(additionalPropertiesSchema.getAllOf(), "allOf inside additionalProperties should be merged for OAS 3.1"); + assertNotNull(additionalPropertiesSchema.getProperties()); + assertTrue(additionalPropertiesSchema.getProperties().containsKey("a")); + assertTrue(additionalPropertiesSchema.getProperties().containsKey("b")); + } + + @Test + public void additionalPropertiesWithRefResolveForOas31() { + String yaml = "openapi: 3.1.0\n" + + "info:\n" + + " title: Test\n" + + " version: 1.0.0\n" + + "paths:\n" + + " /example:\n" + + " post:\n" + + " requestBody:\n" + + " content:\n" + + " application/json:\n" + + " schema:\n" + + " type: object\n" + + " additionalProperties:\n" + + " $ref: '#/components/schemas/Extra'\n" + + " responses:\n" + + " '200':\n" + + " description: OK\n" + + "components:\n" + + " schemas:\n" + + " Extra:\n" + + " type: object\n" + + " properties:\n" + + " y:\n" + + " type: string\n"; + + OpenAPI openAPI = new OpenAPIV3Parser().readContents(yaml, null, resolveCombinatorsOptions()).getOpenAPI(); + assertNotNull(openAPI); + + Schema mapSchema = requestBodySchema(openAPI); + Object additionalProperties = mapSchema.getAdditionalProperties(); + assertTrue(additionalProperties instanceof Schema); + Schema additionalPropertiesSchema = (Schema) additionalProperties; + assertNull(additionalPropertiesSchema.get$ref(), "additionalProperties $ref should be resolved for OAS 3.1"); + assertNotNull(additionalPropertiesSchema.getProperties()); + assertTrue(additionalPropertiesSchema.getProperties().containsKey("y")); + } +} diff --git a/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/ResolveOas31OnlySchemaFieldsTest.java b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/ResolveOas31OnlySchemaFieldsTest.java new file mode 100644 index 0000000000..55b1c27ff2 --- /dev/null +++ b/modules/swagger-parser-v3/src/test/java/io/swagger/v3/parser/test/ResolveOas31OnlySchemaFieldsTest.java @@ -0,0 +1,398 @@ +package io.swagger.v3.parser.test; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.parser.OpenAPIV3Parser; +import io.swagger.v3.parser.core.models.ParseOptions; +import org.testng.annotations.Test; + +import java.util.Map; + +import static org.testng.Assert.*; + +public class ResolveOas31OnlySchemaFieldsTest { + + private ParseOptions resolveCombinatorsOptions() { + ParseOptions options = new ParseOptions(); + options.setResolve(true); + options.setResolveFully(true); + options.setResolveCombinators(true); + return options; + } + + private Schema requestBodySchema(OpenAPI openAPI) { + return openAPI.getPaths().get("/example").getPost() + .getRequestBody().getContent().get("application/json").getSchema(); + } + + @Test + public void notResolvesAndMergesForOas30() { + String yaml = "openapi: 3.0.0\n" + + "info:\n title: Test\n version: 1.0.0\n" + + "paths:\n" + + " /example:\n" + + " post:\n" + + " requestBody:\n" + + " content:\n" + + " application/json:\n" + + " schema:\n" + + " not:\n" + + " type: object\n" + + " allOf:\n" + + " - type: object\n" + + " properties:\n" + + " a:\n" + + " type: string\n" + + " - type: object\n" + + " properties:\n" + + " b:\n" + + " type: string\n" + + " responses:\n" + + " '200':\n" + + " description: OK\n"; + + OpenAPI openAPI = new OpenAPIV3Parser().readContents(yaml, null, resolveCombinatorsOptions()).getOpenAPI(); + Schema schema = requestBodySchema(openAPI); + assertNotNull(schema.getNot(), "not should be resolved (it should not itself disappear)"); + assertNull(schema.getNot().getAllOf()); + assertNotNull(schema.getNot().getProperties()); + assertTrue(schema.getNot().getProperties().containsKey("a")); + assertTrue(schema.getNot().getProperties().containsKey("b")); + } + + @Test + public void notResolvesAndMergesForOas31() { + String yaml = "openapi: 3.1.0\n" + + "info:\n title: Test\n version: 1.0.0\n" + + "paths:\n" + + " /example:\n" + + " post:\n" + + " requestBody:\n" + + " content:\n" + + " application/json:\n" + + " schema:\n" + + " not:\n" + + " type: object\n" + + " allOf:\n" + + " - type: object\n" + + " properties:\n" + + " a:\n" + + " type: string\n" + + " - type: object\n" + + " properties:\n" + + " b:\n" + + " type: string\n" + + " responses:\n" + + " '200':\n" + + " description: OK\n"; + + OpenAPI openAPI = new OpenAPIV3Parser().readContents(yaml, null, resolveCombinatorsOptions()).getOpenAPI(); + Schema schema = requestBodySchema(openAPI); + assertNotNull(schema.getNot()); + assertNull(schema.getNot().getAllOf()); + assertNotNull(schema.getNot().getProperties()); + assertTrue(schema.getNot().getProperties().containsKey("a")); + assertTrue(schema.getNot().getProperties().containsKey("b")); + } + + @Test + public void ifThenElseResolveAndMergeForOas31() { + String yaml = "openapi: 3.1.0\n" + + "info:\n title: Test\n version: 1.0.0\n" + + "paths:\n" + + " /example:\n" + + " post:\n" + + " requestBody:\n" + + " content:\n" + + " application/json:\n" + + " schema:\n" + + " if:\n" + + " $ref: '#/components/schemas/Cond'\n" + + " then:\n" + + " type: object\n" + + " allOf:\n" + + " - type: object\n" + + " properties:\n" + + " a:\n" + + " type: string\n" + + " - type: object\n" + + " properties:\n" + + " b:\n" + + " type: string\n" + + " else:\n" + + " type: object\n" + + " allOf:\n" + + " - type: object\n" + + " properties:\n" + + " x:\n" + + " type: string\n" + + " - type: object\n" + + " properties:\n" + + " y:\n" + + " type: string\n" + + " responses:\n" + + " '200':\n" + + " description: OK\n" + + "components:\n" + + " schemas:\n" + + " Cond:\n" + + " type: object\n" + + " properties:\n" + + " c:\n" + + " type: string\n"; + + OpenAPI openAPI = new OpenAPIV3Parser().readContents(yaml, null, resolveCombinatorsOptions()).getOpenAPI(); + Schema schema = requestBodySchema(openAPI); + + assertNotNull(schema.getIf()); + assertNull(schema.getIf().get$ref(), "if's $ref should be resolved"); + assertNotNull(schema.getIf().getProperties()); + assertTrue(schema.getIf().getProperties().containsKey("c")); + + assertNull(schema.getThen().getAllOf()); + assertNotNull(schema.getThen().getProperties()); + assertTrue(schema.getThen().getProperties().containsKey("a")); + assertTrue(schema.getThen().getProperties().containsKey("b")); + assertNull(schema.getElse().getAllOf()); + assertNotNull(schema.getElse().getProperties()); + assertTrue(schema.getElse().getProperties().containsKey("x")); + assertTrue(schema.getElse().getProperties().containsKey("y")); + } + + @Test + public void containsResolvesAndMergesForOas31() { + String yaml = "openapi: 3.1.0\n" + + "info:\n title: Test\n version: 1.0.0\n" + + "paths:\n" + + " /example:\n" + + " post:\n" + + " requestBody:\n" + + " content:\n" + + " application/json:\n" + + " schema:\n" + + " type: array\n" + + " contains:\n" + + " type: object\n" + + " allOf:\n" + + " - type: object\n" + + " properties:\n" + + " a:\n" + + " type: string\n" + + " - type: object\n" + + " properties:\n" + + " b:\n" + + " type: string\n" + + " responses:\n" + + " '200':\n" + + " description: OK\n"; + + OpenAPI openAPI = new OpenAPIV3Parser().readContents(yaml, null, resolveCombinatorsOptions()).getOpenAPI(); + Schema schema = requestBodySchema(openAPI); + assertNotNull(schema.getContains()); + assertNull(schema.getContains().getAllOf()); + assertNotNull(schema.getContains().getProperties()); + assertTrue(schema.getContains().getProperties().containsKey("a")); + assertTrue(schema.getContains().getProperties().containsKey("b")); + } + + @Test + public void propertyNamesAndContentSchemaResolveForOas31() { + String yaml = "openapi: 3.1.0\n" + + "info:\n title: Test\n version: 1.0.0\n" + + "paths:\n" + + " /example:\n" + + " post:\n" + + " requestBody:\n" + + " content:\n" + + " application/json:\n" + + " schema:\n" + + " type: object\n" + + " propertyNames:\n" + + " allOf:\n" + + " - type: string\n" + + " minLength: 1\n" + + " - type: string\n" + + " pattern: '^[a-z]'\n" + + " contentSchema:\n" + + " $ref: '#/components/schemas/Name'\n" + + " responses:\n" + + " '200':\n" + + " description: OK\n" + + "components:\n" + + " schemas:\n" + + " Name:\n" + + " type: string\n" + + " minLength: 1\n"; + + OpenAPI openAPI = new OpenAPIV3Parser().readContents(yaml, null, resolveCombinatorsOptions()).getOpenAPI(); + Schema schema = requestBodySchema(openAPI); + + assertNotNull(schema.getPropertyNames()); + assertNull(schema.getPropertyNames().getAllOf(), "propertyNames allOf should be merged"); + assertEquals(schema.getPropertyNames().getMinLength(), Integer.valueOf(1)); + assertEquals(schema.getPropertyNames().getPattern(), "^[a-z]"); + + assertNotNull(schema.getContentSchema()); + assertNull(schema.getContentSchema().get$ref(), "contentSchema $ref should be resolved"); + assertEquals(schema.getContentSchema().getMinLength(), Integer.valueOf(1)); + } + + @Test + public void unevaluatedPropertiesAndUnevaluatedItemsResolveForOas31() { + String yaml = "openapi: 3.1.0\n" + + "info:\n title: Test\n version: 1.0.0\n" + + "paths:\n" + + " /example:\n" + + " post:\n" + + " requestBody:\n" + + " content:\n" + + " application/json:\n" + + " schema:\n" + + " type: object\n" + + " unevaluatedProperties:\n" + + " allOf:\n" + + " - type: object\n" + + " properties:\n" + + " y:\n" + + " type: string\n" + + " - type: object\n" + + " properties:\n" + + " z:\n" + + " type: string\n" + + " unevaluatedItems:\n" + + " $ref: '#/components/schemas/Extra'\n" + + " responses:\n" + + " '200':\n" + + " description: OK\n" + + "components:\n" + + " schemas:\n" + + " Extra:\n" + + " type: object\n" + + " properties:\n" + + " y:\n" + + " type: string\n"; + + OpenAPI openAPI = new OpenAPIV3Parser().readContents(yaml, null, resolveCombinatorsOptions()).getOpenAPI(); + Schema schema = requestBodySchema(openAPI); + + assertNotNull(schema.getUnevaluatedProperties()); + assertNull(schema.getUnevaluatedProperties().getAllOf()); + assertTrue(schema.getUnevaluatedProperties().getProperties().containsKey("y")); + assertTrue(schema.getUnevaluatedProperties().getProperties().containsKey("z")); + + assertNotNull(schema.getUnevaluatedItems()); + assertNull(schema.getUnevaluatedItems().get$ref()); + assertTrue(schema.getUnevaluatedItems().getProperties().containsKey("y")); + } + + // No parsed-input test for additionalItems: OpenAPI 3.1 uses JSON Schema 2020-12, + // where additionalItems is not a recognized keyword. OpenAPIDeserializer therefore + // stores it as an extension instead of Schema.additionalItems. Programmatically + // populated additionalItems is handled separately. + + @Test + public void prefixItemsResolveAndMergeForOas31() { + String yaml = "openapi: 3.1.0\n" + + "info:\n title: Test\n version: 1.0.0\n" + + "paths:\n" + + " /example:\n" + + " post:\n" + + " requestBody:\n" + + " content:\n" + + " application/json:\n" + + " schema:\n" + + " type: array\n" + + " prefixItems:\n" + + " - $ref: '#/components/schemas/First'\n" + + " - type: object\n" + + " allOf:\n" + + " - type: object\n" + + " properties:\n" + + " a:\n" + + " type: string\n" + + " - type: object\n" + + " properties:\n" + + " b:\n" + + " type: string\n" + + " responses:\n" + + " '200':\n" + + " description: OK\n" + + "components:\n" + + " schemas:\n" + + " First:\n" + + " type: string\n"; + + OpenAPI openAPI = new OpenAPIV3Parser().readContents(yaml, null, resolveCombinatorsOptions()).getOpenAPI(); + Schema schema = requestBodySchema(openAPI); + + assertNotNull(schema.getPrefixItems()); + assertEquals(schema.getPrefixItems().size(), 2); + Schema first = (Schema) schema.getPrefixItems().get(0); + assertNull(first.get$ref(), "prefixItems[0] $ref should be resolved"); + + Schema second = (Schema) schema.getPrefixItems().get(1); + assertNull(second.getAllOf()); + assertNotNull(second.getProperties()); + assertTrue(second.getProperties().containsKey("a")); + assertTrue(second.getProperties().containsKey("b")); + } + + @Test + public void patternPropertiesAndDependentSchemasResolveAndMergeForOas31() { + String yaml = "openapi: 3.1.0\n" + + "info:\n title: Test\n version: 1.0.0\n" + + "paths:\n" + + " /example:\n" + + " post:\n" + + " requestBody:\n" + + " content:\n" + + " application/json:\n" + + " schema:\n" + + " type: object\n" + + " patternProperties:\n" + + " '^x-':\n" + + " type: object\n" + + " allOf:\n" + + " - type: object\n" + + " properties:\n" + + " a:\n" + + " type: string\n" + + " - type: object\n" + + " properties:\n" + + " b:\n" + + " type: string\n" + + " dependentSchemas:\n" + + " foo:\n" + + " allOf:\n" + + " - type: object\n" + + " properties:\n" + + " y:\n" + + " type: string\n" + + " - type: object\n" + + " properties:\n" + + " z:\n" + + " type: string\n" + + " responses:\n" + + " '200':\n" + + " description: OK\n"; + + OpenAPI openAPI = new OpenAPIV3Parser().readContents(yaml, null, resolveCombinatorsOptions()).getOpenAPI(); + Schema schema = requestBodySchema(openAPI); + + assertNotNull(schema.getPatternProperties()); + Map patternProperties = schema.getPatternProperties(); + Schema patternProp = patternProperties.get("^x-"); + assertNull(patternProp.getAllOf()); + assertNotNull(patternProp.getProperties()); + assertTrue(patternProp.getProperties().containsKey("a")); + assertTrue(patternProp.getProperties().containsKey("b")); + + assertNotNull(schema.getDependentSchemas()); + Map dependentSchemas = schema.getDependentSchemas(); + Schema fooDependent = dependentSchemas.get("foo"); + assertNotNull(fooDependent); + assertNull(fooDependent.getAllOf(), "dependentSchemas allOf should be merged"); + assertTrue(fooDependent.getProperties().containsKey("y")); + assertTrue(fooDependent.getProperties().containsKey("z")); + } +}