fix: ExampleGenerator correctly generates allOf composed schemas (#17499)

* fix: ExampleGenerator correctly generates allOf composed schemas

Changes the previous behavior of generating `null` examples for allOf composed schemas.

Fixes #17497

* fix: ExampleGenerator correctly generates anyOf and oneOf composed schemas

Changes the previous behavior of generating `null` examples for anyOf and oneOf composed schemas.

To generate a oneOf/anyOf example, we generate the example using the first valid schema available. In case of a $ref, we use the first valid reference.

Fixes #17497
This commit is contained in:
Alexis Couvreur 2024-01-09 22:04:11 -05:00 committed by GitHub
parent 8bab0ceb53
commit dd5c7e3b9a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 288 additions and 9 deletions

View File

@ -351,15 +351,57 @@ public class ExampleGenerator {
return schema.getExample();
} else if (schema.getProperties() != null) {
LOGGER.debug("Creating example from model values");
for (Object propertyName : schema.getProperties().keySet()) {
Schema property = (Schema) schema.getProperties().get(propertyName.toString());
values.put(propertyName.toString(), resolvePropertyToExample(propertyName.toString(), mediaType, property, processedModels));
traverseSchemaProperties(mediaType, schema, processedModels, values);
schema.setExample(values);
return schema.getExample();
} else if (ModelUtils.isAllOf(schema) || ModelUtils.isAllOfWithProperties(schema)) {
LOGGER.debug("Resolving allOf model '{}' to example", name);
List<Schema> interfaces = schema.getAllOf();
for (Schema composed : interfaces) {
traverseSchemaProperties(mediaType, composed, processedModels, values);
if (composed.get$ref() != null) {
String ref = ModelUtils.getSimpleRef(composed.get$ref());
Schema resolved = ModelUtils.getSchema(openAPI, ref);
if (resolved != null) {
traverseSchemaProperties(mediaType, resolved, processedModels, values);
}
}
}
schema.setExample(values);
return schema.getExample();
} else if (ModelUtils.isAnyOf(schema) || ModelUtils.isOneOf(schema)) {
LOGGER.debug("Resolving anyOf/oneOf model '{}' using the first schema to example", name);
Optional<Schema> found = ModelUtils.getInterfaces(schema)
.stream()
.filter(this::hasValidRef)
.findFirst();
if (found.isEmpty()) {
return null;
}
return resolvePropertyToExample(name, mediaType, found.get(), processedModels);
} else {
// TODO log an error message as the model does not have any properties
return null;
}
}
private void traverseSchemaProperties(String mediaType, Schema schema, Set<String> processedModels, Map<String, Object> values) {
if (schema.getProperties() != null) {
for (Object propertyName : schema.getProperties().keySet()) {
Schema property = (Schema) schema.getProperties().get(propertyName.toString());
values.put(propertyName.toString(), resolvePropertyToExample(propertyName.toString(), mediaType, property, processedModels));
}
}
}
private boolean hasValidRef(Schema schema) {
if (schema.get$ref() != null) {
String ref = ModelUtils.getSimpleRef(schema.get$ref());
Schema resolved = ModelUtils.getSchema(openAPI, ref);
return resolved != null;
}
return true;
}
}

View File

@ -151,4 +151,91 @@ public class ExampleGeneratorTest {
assertEquals(String.format(Locale.ROOT, "{%n \"example_schema_property\" : \"example schema property value\"%n}"), examples.get(0).get("example"));
assertEquals("200", examples.get(0).get("statusCode"));
}
@Test
public void generateFromResponseSchemaWithAllOfComposedModel() {
OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/example_generator_test.yaml");
new InlineModelResolver().flatten(openAPI);
ExampleGenerator exampleGenerator = new ExampleGenerator(openAPI.getComponents().getSchemas(), openAPI);
Set<String> mediaTypeKeys = new TreeSet<>();
mediaTypeKeys.add("application/json");
List<Map<String, String>> examples = exampleGenerator.generateFromResponseSchema(
"200",
openAPI
.getPaths()
.get("/generate_from_response_schema_with_allOf_composed_model")
.getGet()
.getResponses()
.get("200")
.getContent()
.get("application/json")
.getSchema(),
mediaTypeKeys
);
assertEquals(1, examples.size());
assertEquals("application/json", examples.get(0).get("contentType"));
assertEquals(String.format(Locale.ROOT, "{%n \"example_schema_property_composed\" : \"example schema property value composed\",%n \"example_schema_property\" : \"example schema property value\"%n}"), examples.get(0).get("example"));
assertEquals("200", examples.get(0).get("statusCode"));
}
@Test
public void generateFromResponseSchemaWithOneOfComposedModel() {
OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/example_generator_test.yaml");
new InlineModelResolver().flatten(openAPI);
ExampleGenerator exampleGenerator = new ExampleGenerator(openAPI.getComponents().getSchemas(), openAPI);
Set<String> mediaTypeKeys = new TreeSet<>();
mediaTypeKeys.add("application/json");
List<Map<String, String>> examples = exampleGenerator.generateFromResponseSchema(
"200",
openAPI
.getPaths()
.get("/generate_from_response_schema_with_oneOf_composed_model")
.getGet()
.getResponses()
.get("200")
.getContent()
.get("application/json")
.getSchema(),
mediaTypeKeys
);
assertEquals(1, examples.size());
assertEquals("application/json", examples.get(0).get("contentType"));
assertEquals(String.format(Locale.ROOT, "{%n \"example_schema_property\" : \"example schema property value\"%n}"), examples.get(0).get("example"));
assertEquals("200", examples.get(0).get("statusCode"));
}
@Test
public void generateFromResponseSchemaWithAnyOfComposedModel() {
OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_0/example_generator_test.yaml");
new InlineModelResolver().flatten(openAPI);
ExampleGenerator exampleGenerator = new ExampleGenerator(openAPI.getComponents().getSchemas(), openAPI);
Set<String> mediaTypeKeys = new TreeSet<>();
mediaTypeKeys.add("application/json");
List<Map<String, String>> examples = exampleGenerator.generateFromResponseSchema(
"200",
openAPI
.getPaths()
.get("/generate_from_response_schema_with_anyOf_composed_model")
.getGet()
.getResponses()
.get("200")
.getContent()
.get("application/json")
.getSchema(),
mediaTypeKeys
);
assertEquals(1, examples.size());
assertEquals("application/json", examples.get(0).get("contentType"));
assertEquals(String.format(Locale.ROOT, "{%n \"example_schema_property\" : \"example schema property value\"%n}"), examples.get(0).get("example"));
assertEquals("200", examples.get(0).get("statusCode"));
}
}

View File

@ -53,7 +53,7 @@ paths:
example: primitive types example value
/generate_from_response_schema_with_model:
get:
operationId: generateFromResponseSchemaWithArrayOfPrimitiveTypes
operationId: generateFromResponseSchemaWithModel
responses:
'200':
description: successful operation
@ -61,6 +61,36 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ExampleSchema'
/generate_from_response_schema_with_allOf_composed_model:
get:
operationId: generateFromResponseSchemaWithAllOfModel
responses:
'200':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/ExampleAllOfSchema'
/generate_from_response_schema_with_anyOf_composed_model:
get:
operationId: generateFromResponseSchemaWithAnyOfModel
responses:
'200':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/ExampleAnyOfSchema'
/generate_from_response_schema_with_oneOf_composed_model:
get:
operationId: generateFromResponseSchemaWithOneOfModel
responses:
'200':
description: successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/ExampleOneOfSchema'
components:
schemas:
StringSchema:
@ -72,3 +102,30 @@ components:
example_schema_property:
type: string
example: example schema property value
ExampleAllOfSchema:
type: object
allOf:
- $ref: '#/components/schemas/ExampleSchema'
- type: object
properties:
example_schema_property_composed:
type: string
example: example schema property value composed
ExampleAnyOfSchema:
type: object
anyOf:
- $ref: '#/components/schemas/ExampleSchema'
- type: object
properties:
example_schema_property_composed:
type: string
example: example schema property value composed
ExampleOneOfSchema:
type: object
oneOf:
- $ref: '#/components/schemas/ExampleSchema'
- type: object
properties:
example_schema_property_composed:
type: string
example: example schema property value composed

View File

@ -1942,6 +1942,10 @@ components:
otherProperty:
type: string
type: object
example:
otherProperty: otherProperty
nullableProperty: nullableProperty
type: ChildWithNullable
StringBooleanMap:
additionalProperties:
type: boolean

View File

@ -1942,6 +1942,10 @@ components:
otherProperty:
type: string
type: object
example:
otherProperty: otherProperty
nullableProperty: nullableProperty
type: ChildWithNullable
StringBooleanMap:
additionalProperties:
type: boolean

View File

@ -2411,7 +2411,7 @@ components:
description: Value object
example:
name: variable_1
value: null
value: Scalar
properties:
name:
example: variable_1

View File

@ -1942,6 +1942,10 @@ components:
otherProperty:
type: string
type: object
example:
otherProperty: otherProperty
nullableProperty: nullableProperty
type: ChildWithNullable
StringBooleanMap:
additionalProperties:
type: boolean

View File

@ -1942,6 +1942,10 @@ components:
otherProperty:
type: string
type: object
example:
otherProperty: otherProperty
nullableProperty: nullableProperty
type: ChildWithNullable
StringBooleanMap:
additionalProperties:
type: boolean

View File

@ -1942,6 +1942,10 @@ components:
otherProperty:
type: string
type: object
example:
otherProperty: otherProperty
nullableProperty: nullableProperty
type: ChildWithNullable
StringBooleanMap:
additionalProperties:
type: boolean

View File

@ -1942,6 +1942,10 @@ components:
otherProperty:
type: string
type: object
example:
otherProperty: otherProperty
nullableProperty: nullableProperty
type: ChildWithNullable
StringBooleanMap:
additionalProperties:
type: boolean

View File

@ -1942,6 +1942,10 @@ components:
otherProperty:
type: string
type: object
example:
otherProperty: otherProperty
nullableProperty: nullableProperty
type: ChildWithNullable
StringBooleanMap:
additionalProperties:
type: boolean

View File

@ -1942,6 +1942,10 @@ components:
otherProperty:
type: string
type: object
example:
otherProperty: otherProperty
nullableProperty: nullableProperty
type: ChildWithNullable
StringBooleanMap:
additionalProperties:
type: boolean

View File

@ -1942,6 +1942,10 @@ components:
otherProperty:
type: string
type: object
example:
otherProperty: otherProperty
nullableProperty: nullableProperty
type: ChildWithNullable
StringBooleanMap:
additionalProperties:
type: boolean

View File

@ -49,4 +49,7 @@ components:
\ characters. The sanitization rules should make it possible to generate a\
\ language-specific classname with allowed characters in that programming\
\ language."
example:
prop2: prop2
objectType: objectType

View File

@ -71,7 +71,7 @@ public interface BarApi {
getRequest().ifPresent(request -> {
for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) {
if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) {
String exampleString = "{ \"id\" : \"id\", \"fooPropB\" : \"fooPropB\", \"barPropA\" : \"barPropA\" }";
String exampleString = "{ \"foo\" : { \"fooPropA\" : \"fooPropA\", \"fooPropB\" : \"fooPropB\" }, \"id\" : \"id\", \"fooPropB\" : \"fooPropB\", \"barPropA\" : \"barPropA\" }";
ApiUtil.setExampleResponse(request, "application/json", exampleString);
break;
}

View File

@ -71,7 +71,7 @@ public interface FooApi {
getRequest().ifPresent(request -> {
for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) {
if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) {
String exampleString = "null";
String exampleString = "{ \"fooPropA\" : \"fooPropA\", \"fooPropB\" : \"fooPropB\" }";
ApiUtil.setExampleResponse(request, "application/json", exampleString);
break;
}
@ -109,7 +109,7 @@ public interface FooApi {
getRequest().ifPresent(request -> {
for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) {
if (mediaType.isCompatibleWith(MediaType.valueOf("application/json;charset=utf-8"))) {
String exampleString = "[ null, null ]";
String exampleString = "[ { \"fooPropA\" : \"fooPropA\", \"fooPropB\" : \"fooPropB\" }, { \"fooPropA\" : \"fooPropA\", \"fooPropB\" : \"fooPropB\" } ]";
ApiUtil.setExampleResponse(request, "application/json;charset=utf-8", exampleString);
break;
}

View File

@ -189,7 +189,9 @@ components:
allOf:
- $ref: '#/components/schemas/Entity'
example:
foo: null
foo:
fooPropA: fooPropA
fooPropB: fooPropB
id: id
fooPropB: fooPropB
barPropA: barPropA

View File

@ -1902,6 +1902,10 @@ components:
otherProperty:
type: string
type: object
example:
otherProperty: otherProperty
nullableProperty: nullableProperty
type: ChildWithNullable
StringBooleanMap:
additionalProperties:
type: boolean

View File

@ -1902,6 +1902,10 @@ components:
otherProperty:
type: string
type: object
example:
otherProperty: otherProperty
nullableProperty: nullableProperty
type: ChildWithNullable
StringBooleanMap:
additionalProperties:
type: boolean

View File

@ -1942,6 +1942,10 @@ components:
otherProperty:
type: string
type: object
example:
otherProperty: otherProperty
nullableProperty: nullableProperty
type: ChildWithNullable
StringBooleanMap:
additionalProperties:
type: boolean

View File

@ -1942,6 +1942,10 @@ components:
otherProperty:
type: string
type: object
example:
otherProperty: otherProperty
nullableProperty: nullableProperty
type: ChildWithNullable
StringBooleanMap:
additionalProperties:
type: boolean

View File

@ -1902,6 +1902,10 @@ components:
otherProperty:
type: string
type: object
example:
otherProperty: otherProperty
nullableProperty: nullableProperty
type: ChildWithNullable
StringBooleanMap:
additionalProperties:
type: boolean

View File

@ -1902,6 +1902,10 @@ components:
otherProperty:
type: string
type: object
example:
otherProperty: otherProperty
nullableProperty: nullableProperty
type: ChildWithNullable
StringBooleanMap:
additionalProperties:
type: boolean

View File

@ -1902,6 +1902,10 @@ components:
otherProperty:
type: string
type: object
example:
otherProperty: otherProperty
nullableProperty: nullableProperty
type: ChildWithNullable
StringBooleanMap:
additionalProperties:
type: boolean

View File

@ -1902,6 +1902,10 @@ components:
otherProperty:
type: string
type: object
example:
otherProperty: otherProperty
nullableProperty: nullableProperty
type: ChildWithNullable
StringBooleanMap:
additionalProperties:
type: boolean

View File

@ -1902,6 +1902,10 @@ components:
otherProperty:
type: string
type: object
example:
otherProperty: otherProperty
nullableProperty: nullableProperty
type: ChildWithNullable
StringBooleanMap:
additionalProperties:
type: boolean

View File

@ -1902,6 +1902,10 @@ components:
otherProperty:
type: string
type: object
example:
otherProperty: otherProperty
nullableProperty: nullableProperty
type: ChildWithNullable
StringBooleanMap:
additionalProperties:
type: boolean

View File

@ -1902,6 +1902,10 @@ components:
otherProperty:
type: string
type: object
example:
otherProperty: otherProperty
nullableProperty: nullableProperty
type: ChildWithNullable
StringBooleanMap:
additionalProperties:
type: boolean

View File

@ -1902,6 +1902,10 @@ components:
otherProperty:
type: string
type: object
example:
otherProperty: otherProperty
nullableProperty: nullableProperty
type: ChildWithNullable
StringBooleanMap:
additionalProperties:
type: boolean

View File

@ -1902,6 +1902,10 @@ components:
otherProperty:
type: string
type: object
example:
otherProperty: otherProperty
nullableProperty: nullableProperty
type: ChildWithNullable
StringBooleanMap:
additionalProperties:
type: boolean