Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -984,7 +984,13 @@ public Schema normalizeSchema(Schema schema, Set<Schema> visitedSchemas) {

if (ModelUtils.isArraySchema(schema)) { // array
Schema result = normalizeArraySchema(schema);
normalizeSchema(result.getItems(), visitedSchemas);
if (result.getItems() != null) {
boolean isTypelessExt = isTypelessExternalObjectSchema(result.getItems());
Schema normalizedItems = normalizeSchema(result.getItems(), visitedSchemas);
if (isTypelessExt) {
result.setItems(normalizedItems);
}
}
return result;
} else if (ModelUtils.isOneOf(schema)) { // oneOf
return normalizeOneOf(schema, visitedSchemas);
Expand Down Expand Up @@ -1047,8 +1053,9 @@ public Schema normalizeSchema(Schema schema, Set<Schema> visitedSchemas) {
anyTypeNullable.setNullable(true);
result.setAdditionalProperties(anyTypeNullable);
} else {
boolean isTypelessExt = isTypelessExternalObjectSchema(additionalProperties);
Schema normalized = normalizeSchema(additionalProperties, visitedSchemas);
if (getRule(NORMALIZE_31SPEC)) {
if (getRule(NORMALIZE_31SPEC) || isTypelessExt) {
// capture the normalized value schema (e.g. an OAS 3.1 `type: [array, "null"]`
// value is rewritten to a proper array schema), which would otherwise be lost.
result.setAdditionalProperties(normalized);
Comment thread
josephaxisa marked this conversation as resolved.
Expand Down Expand Up @@ -1178,7 +1185,49 @@ protected Schema normalizeMapSchema(Schema schema) {
return processSetMapToNullable(result);
}

private boolean isTypelessExternalObjectSchema(Schema schema) {
if (openAPI != null && openAPI.getExtensions() != null &&
openAPI.getExtensions().containsKey("x-original-swagger-version")) {
return false;
}
return schema instanceof ObjectSchema &&
schema.getJsonSchema() != null &&
!schema.getJsonSchema().containsKey("type") &&
!schema.getJsonSchema().containsKey("properties") &&
!schema.getJsonSchema().containsKey("items") &&
schema.getProperties() == null &&
schema.getItems() == null &&
schema.getAdditionalProperties() == null &&
schema.getRequired() == null &&
schema.getMinProperties() == null &&
schema.getMaxProperties() == null &&
schema.getPatternProperties() == null &&
schema.getDiscriminator() == null &&
schema.getEnum() == null;
}

protected Schema normalizeSimpleSchema(Schema schema, Set<Schema> visitedSchemas) {
if (isTypelessExternalObjectSchema(schema)) {
// When swagger-parser resolves an external file via Jackson (ModelDeserializer),
// a typeless OAS 3.0 schema is deserialized into ObjectSchema (type="object")
// with raw fields recorded in jsonSchema (without a "type" key). Convert it
// back to a base Schema (type=null) so generators treat it as AnyType.
Schema sc = new Schema<>();
ModelUtils.copyMetadata(schema, sc);
sc.setPattern(schema.getPattern());
sc.setFormat(schema.getFormat());
sc.setMultipleOf(schema.getMultipleOf());
sc.setUniqueItems(schema.getUniqueItems());
sc.setExclusiveMinimum(schema.getExclusiveMinimum());
sc.setExclusiveMaximum(schema.getExclusiveMaximum());
sc.setExclusiveMinimumValue(schema.getExclusiveMinimumValue());
sc.setExclusiveMaximumValue(schema.getExclusiveMaximumValue());
sc.setNot(schema.getNot());
sc.setXml(schema.getXml());
sc.setExternalDocs(schema.getExternalDocs());
sc.setJsonSchema(schema.getJsonSchema());
schema = sc;
}
Schema result = processNormalize31Spec(schema, visitedSchemas);
return processSetPrimitiveTypesToNullable(result);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@

package org.openapitools.codegen;

import io.swagger.parser.OpenAPIParser;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.media.*;
import io.swagger.v3.oas.models.parameters.Parameter;
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.parser.core.models.ParseOptions;
import org.openapitools.codegen.utils.ModelUtils;
import org.testng.annotations.Test;

Expand Down Expand Up @@ -2263,5 +2265,132 @@ public void testOpenAPINormalizer31SpecNullMapAdditionalProperties() {
assertNotNull(errorsValue.getItems());
}

@Test
public void testTypelessSchemaInReferencedFileRemainsAnyType() throws IOException {
Path tempDir = Files.createTempDirectory("openapi-normalizer-typeless-ext");
Path pathItemFile = tempDir.resolve("variables-path.yaml");
Files.writeString(pathItemFile,
"get:\n" +
" operationId: getVariable\n" +
" responses:\n" +
" '200':\n" +
" description: ok\n" +
" content:\n" +
" application/json:\n" +
" schema:\n" +
" description: Arbitrary variable value\n" +
" example: John Doe\n" +
" '201':\n" +
" description: list of values\n" +
" content:\n" +
" application/json:\n" +
" schema:\n" +
" type: array\n" +
" items:\n" +
" description: Arbitrary item value\n" +
" example: item1\n");

Path schemaFile = tempDir.resolve("variable-value.yaml");
Files.writeString(schemaFile,
"description: Arbitrary variable value\n" +
"example: John Doe\n");

Path explicitObjectFile = tempDir.resolve("explicit-object.yaml");
Files.writeString(explicitObjectFile,
"type: object\n" +
"description: Explicit free-form object\n");

Path requiredConstraintFile = tempDir.resolve("required-only.yaml");
Files.writeString(requiredConstraintFile,
"required:\n" +
" - id\n");

Path rootSpec = tempDir.resolve("openapi.yaml");
Files.writeString(rootSpec,
"openapi: 3.0.3\n" +
"info:\n" +
" title: Typeless External Schema Test\n" +
" version: 1.0.0\n" +
"paths:\n" +
" /variables:\n" +
" $ref: './variables-path.yaml'\n" +
"components:\n" +
" schemas:\n" +
" VariableHolder:\n" +
" type: object\n" +
" properties:\n" +
" value:\n" +
" $ref: './variable-value.yaml'\n" +
" values:\n" +
" type: array\n" +
" items:\n" +
" $ref: './variable-value.yaml'\n" +
" explicitObj:\n" +
" $ref: './explicit-object.yaml'\n" +
" requiredOnly:\n" +
" $ref: './required-only.yaml'\n");

ParseOptions parseOptions = new ParseOptions();
parseOptions.setResolve(true);
parseOptions.setResolveResponses(true);
OpenAPI openAPI = new OpenAPIParser().readLocation(rootSpec.toString(), null, parseOptions).getOpenAPI();
assertNotNull(openAPI);

OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, new HashMap<>());
openAPINormalizer.normalize();

Schema<?> responseSchema = openAPI.getPaths().get("/variables")
.getGet().getResponses().get("200")
.getContent().get("application/json").getSchema();
assertNotNull(responseSchema);
assertNull(responseSchema.getType());
assertFalse(ModelUtils.isObjectSchema(responseSchema));
assertTrue(ModelUtils.isAnyType(responseSchema));
assertFalse(ModelUtils.isFreeFormObject(responseSchema, openAPI));
assertEquals(responseSchema.getDescription(), "Arbitrary variable value");
assertEquals(responseSchema.getExample(), "John Doe");

Schema<?> componentSchema = openAPI.getComponents().getSchemas().get("variable-value");
Comment thread
josephaxisa marked this conversation as resolved.
assertNotNull(componentSchema);
assertNull(componentSchema.getType());
assertFalse(ModelUtils.isObjectSchema(componentSchema));
assertTrue(ModelUtils.isAnyType(componentSchema));
assertFalse(ModelUtils.isFreeFormObject(componentSchema, openAPI));
assertEquals(componentSchema.getDescription(), "Arbitrary variable value");
assertEquals(componentSchema.getExample(), "John Doe");

Schema<?> arrayResponseSchema = openAPI.getPaths().get("/variables")
.getGet().getResponses().get("201")
.getContent().get("application/json").getSchema();
assertNotNull(arrayResponseSchema);
Schema<?> inlineArrayItemSchema = arrayResponseSchema.getItems();
assertNotNull(inlineArrayItemSchema);
assertNull(inlineArrayItemSchema.getType());
assertFalse(ModelUtils.isObjectSchema(inlineArrayItemSchema));
assertTrue(ModelUtils.isAnyType(inlineArrayItemSchema));
assertEquals(inlineArrayItemSchema.getDescription(), "Arbitrary item value");

Schema<?> holderSchema = openAPI.getComponents().getSchemas().get("VariableHolder");
assertNotNull(holderSchema);
Schema<?> valuesProp = (Schema<?>) holderSchema.getProperties().get("values");
assertNotNull(valuesProp);
assertNotNull(valuesProp.getItems());
Schema<?> resolvedHolderItem = ModelUtils.getReferencedSchema(openAPI, valuesProp.getItems());
assertNotNull(resolvedHolderItem);
assertNull(resolvedHolderItem.getType());
assertFalse(ModelUtils.isObjectSchema(resolvedHolderItem));
assertTrue(ModelUtils.isAnyType(resolvedHolderItem));

Schema<?> explicitObjSchema = openAPI.getComponents().getSchemas().get("explicit-object");
assertNotNull(explicitObjSchema);
assertEquals(explicitObjSchema.getType(), "object");
assertTrue(ModelUtils.isObjectSchema(explicitObjSchema));
assertTrue(ModelUtils.isFreeFormObject(explicitObjSchema, openAPI));

Schema<?> requiredOnlySchema = openAPI.getComponents().getSchemas().get("required-only");
assertNotNull(requiredOnlySchema);
assertEquals(requiredOnlySchema.getRequired(), Collections.singletonList("id"));
}

}

Loading