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 @@ -1380,6 +1380,14 @@ protected Schema normalizeOneOf(Schema schema, Set<Schema> visitedSchemas) {
}

protected Schema normalizeAnyOf(Schema schema, Set<Schema> visitedSchemas) {
// anyOf does not pass through normalizeSimpleSchema, but a sibling type
// can still express nullability using the OpenAPI 3.1 type array.
if (getRule(NORMALIZE_31SPEC) && schema.getTypes() != null
&& schema.getTypes().size() > 1 && schema.getTypes().contains("null")) {
schema.setNullable(true);
schema.getTypes().remove("null");
}

//transform anyOf into enums if needed
schema = processSimplifyAnyOfEnum(schema);
if (schema.getAnyOf() == null) {
Expand Down Expand Up @@ -1781,9 +1789,15 @@ protected Schema processSimplifyOneOf(Schema schema) {
/**
* Removes the {@code oneOf} from the schema if it is considered to not contain information that the generator can
* currently act upon. The schema is left untouched if all the {@code oneOf} branches contain an OAS 3.1 {@code const}.
* This since that structure can potentially be used for enum interpretation.
* This since that structure can potentially be used for enum interpretation. Untyped validation
* branches are also retained so their predicates survive normalization.
*/
private void clearOneOf(Schema schema) {
if (schema.getOneOf() != null && ((List<Schema>) schema.getOneOf()).stream()
.map(branch -> ModelUtils.getReferencedSchema(openAPI, branch))
.anyMatch(branch -> ModelUtils.getType(branch) == null && ModelUtils.hasValidation(branch))) {
return;
}
if (ModelUtils.isIntegerSchema(schema) || ModelUtils.isNumberSchema(schema) || ModelUtils.isStringSchema(schema)) {
if (!isOneOfOfConsts(schema)) {
schema.setOneOf(null);
Expand Down Expand Up @@ -2283,7 +2297,9 @@ protected Schema processNormalize31Spec(Schema schema, Set<Schema> visitedSchema
normalizeExclusiveMinMax31(schema);

if (schema instanceof JsonSchema &&
schema.get$schema() == null && schema.getTypes() == null && schema.getType() == null) {
schema.get$schema() == null && ModelUtils.isMetadataOnlySchema(schema)
&& !ModelUtils.hasValidation(schema) && schema.getFormat() == null
&& schema.getEnum() == null && schema.getConst() == null) {
// convert any type in v3.1 to empty schema (any type in v3.0 spec), any type example:
// components:
// schemas:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,54 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
return result;
}

@Override
public String getSchemaType(Schema schema) {
if (isPrimitiveWithValidationOnlyComposition(schema)) {
return super.getSchemaType(new Schema().type(ModelUtils.getType(schema)).format(schema.getFormat()));
}
return super.getSchemaType(schema);
}

@Override
public CodegenModel fromModel(String name, Schema schema) {
CodegenModel model = super.fromModel(name, schema);
Schema primitive = ModelUtils.getReferencedSchema(openAPI, schema);
if (!ModelUtils.hasProperties(schema) && schema.getAllOf() != null && schema.getAllOf().size() == 1
&& schema.getAnyOf() == null && schema.getOneOf() == null
&& ModelUtils.getType(schema) == null && schema.getEnum() == null
&& schema.getConst() == null && schema.getNot() == null) {
// Normalization wraps nullable references in a single-member allOf.
primitive = ModelUtils.getReferencedSchema(openAPI, (Schema) schema.getAllOf().get(0));
}
if (isPrimitiveWithValidationOnlyComposition(primitive)) {
model.vendorExtensions.put("x-typescript-primitive-type", getSchemaType(primitive));
}
return model;
}

private boolean isPrimitiveWithValidationOnlyComposition(Schema schema) {
if (!ModelUtils.isPrimitiveType(schema)) {
return false;
}
List<Schema> alternatives = schema.getAnyOf() != null ? schema.getAnyOf() : schema.getOneOf();
if (alternatives == null || alternatives.isEmpty()
|| schema.getEnum() != null || schema.getConst() != null || schema.getNot() != null
|| schema.getAllOf() != null || (schema.getAnyOf() != null && schema.getOneOf() != null)
|| (schema.getTypes() != null && schema.getTypes().size() != 1)) {
return false;
}
// TypeScript cannot express pattern/range/length predicates. Keep the
// enclosing primitive type without removing those predicates from the OAS.
// Enum, const and composed alternatives need their own type representation.
return alternatives.stream().allMatch(branch ->
ModelUtils.isUnsupportedSchema(openAPI, branch)
&& !ModelUtils.isComposedSchema(branch)
&& branch.get$ref() == null
&& branch.getEnum() == null && branch.getConst() == null
&& branch.getNot() == null && branch.getIf() == null
&& branch.getBooleanSchemaValue() == null);
}


@Override
protected void addAdditionPropertiesToCodeGenModel(CodegenModel codegenModel, Schema schema) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2770,13 +2770,21 @@ private static void restoreNullableInList(List<Schema> original, List<Schema> cl
* @return The simplified schema
*/
public static Schema simplifyOneOfAnyOfWithOnlyOneNonNullSubSchema(OpenAPI openAPI, Schema schema, List<Schema> subSchemas) {
if (subSchemas.removeIf(subSchema -> isNullTypeSchema(openAPI, subSchema))) {
boolean hasExplicitType = getType(schema) != null;
if (subSchemas.removeIf(subSchema -> isNullTypeSchema(openAPI, subSchema)) && !hasExplicitType) {
schema.setNullable(true);
}

// if only one element left, simplify to just the element (schema)
if (subSchemas.size() == 1) {
Schema<?> subSchema = subSchemas.get(0);
Schema<?> referencedSchema = getReferencedSchema(openAPI, subSchema);
if (hasExplicitType && getType(referencedSchema) == null
&& (hasValidation(referencedSchema) || isMetadataOnlySchema(referencedSchema))) {
// Untyped annotations or predicates cannot replace the enclosing
// type and its constraints (e.g. type: string, minLength: 2).
return schema;
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
}
// The parser may reuse a $ref schema instance in multiple locations. Clone the
// remaining $ref before applying parent metadata so those locations stay isolated.
if (subSchema.get$ref() != null) {
Expand Down Expand Up @@ -2822,7 +2830,7 @@ public static void deduplicateOneOfSchema(Schema<?> schema) {
/**
* Check if the schema is of type 'null' or schema itself is pointing to null
* <p>
* Return true if the schema's type is 'null' or not specified
* Unspecified types are treated as null only when they do not carry validation constraints.
*
* @param schema Schema
* @param openAPI OpenAPI
Expand Down Expand Up @@ -2876,6 +2884,13 @@ public static boolean isNullTypeSchema(OpenAPI openAPI, Schema schema) {
}
}

// Validation, enum/const, structural schemas and format annotations are not null-only.
if (!"null".equals(schema.getType()) && (hasValidation(schema)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
|| !isMetadataOnlySchema(schema) || schema.getFormat() != null
|| schema.getEnum() != null || schema.getConst() != null)) {
return false;
}

if (schema instanceof JsonSchema) { // 3.1 spec
if (Boolean.TRUE.equals(schema.getNullable())) {
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import type { RequestArgs } from './base{{importFileExtension}}';
import { BASE_PATH, COLLECTION_FORMATS, BaseAPI, RequiredError, operationServerMap } from './base{{importFileExtension}}';

{{#models}}
{{#model}}{{#isEnum}}{{>modelEnum}}{{/isEnum}}{{#oneOf}}{{#-first}}{{>modelOneOf}}{{/-first}}{{/oneOf}}{{^isEnum}}{{^oneOf}}{{>modelGeneric}}{{/oneOf}}{{/isEnum}}{{/model}}
{{#model}}{{#vendorExtensions.x-typescript-primitive-type}}{{>modelGeneric}}{{/vendorExtensions.x-typescript-primitive-type}}{{^vendorExtensions.x-typescript-primitive-type}}{{#isEnum}}{{>modelEnum}}{{/isEnum}}{{#oneOf}}{{#-first}}{{>modelOneOf}}{{/-first}}{{/oneOf}}{{^isEnum}}{{^oneOf}}{{>modelGeneric}}{{/oneOf}}{{/isEnum}}{{/vendorExtensions.x-typescript-primitive-type}}{{/model}}
{{/models}}
{{#apiInfo}}{{#apis}}
{{>apiInner}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ import type { {{class}} } from './{{filename}}{{importFileExtension}}';{{/oneOf}
// @ts-ignore
import type { {{class}} } from './{{filename}}{{importFileExtension}}';{{/imports}}{{/hasOneOf}}{{/hasAllOf}}{{/withSeparateModelsAndApi}}
{{#models}}{{#model}}
{{#isEnum}}{{>modelEnum}}{{/isEnum}}{{#oneOf}}{{#-first}}{{>modelOneOf}}{{/-first}}{{/oneOf}}{{#allOf}}{{#-first}}{{>modelAllOf}}{{/-first}}{{/allOf}}{{^isEnum}}{{^oneOf}}{{^allOf}}{{>modelGeneric}}{{/allOf}}{{/oneOf}}{{/isEnum}}
{{#vendorExtensions.x-typescript-primitive-type}}{{>modelGeneric}}{{/vendorExtensions.x-typescript-primitive-type}}{{^vendorExtensions.x-typescript-primitive-type}}{{#isEnum}}{{>modelEnum}}{{/isEnum}}{{#oneOf}}{{#-first}}{{>modelOneOf}}{{/-first}}{{/oneOf}}{{#allOf}}{{#-first}}{{>modelAllOf}}{{/-first}}{{/allOf}}{{^isEnum}}{{^oneOf}}{{^allOf}}{{>modelGeneric}}{{/allOf}}{{/oneOf}}{{/isEnum}}{{/vendorExtensions.x-typescript-primitive-type}}
{{/model}}{{/models}}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
* {{{description}}}
*/
{{/description}}
{{#vendorExtensions.x-typescript-primitive-type}}
export type {{classname}} = {{{.}}}{{#isNullable}} | null{{/isNullable}};
{{/vendorExtensions.x-typescript-primitive-type}}
{{^vendorExtensions.x-typescript-primitive-type}}
export interface {{classname}} {{#parent}}extends {{{.}}} {{/parent}}{
{{#additionalPropertiesType}}
[key: string]: {{{additionalPropertiesType}}}{{#additionalPropertiesIsAnyType}}{{#hasVars}} | any{{/hasVars}}{{/additionalPropertiesIsAnyType}};
Expand Down Expand Up @@ -63,3 +67,4 @@ export type {{enumName}} = typeof {{enumName}}[keyof typeof {{enumName}}];
{{/isEnum}}
{{/vars}}
{{/hasEnums}}
{{/vendorExtensions.x-typescript-primitive-type}}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.openapitools.codegen.utils.ModelUtils;
import org.testng.annotations.Test;
import org.testng.annotations.DataProvider;

import javax.tools.JavaCompiler;
import javax.tools.ToolProvider;
Expand All @@ -43,6 +44,109 @@

public class OpenAPINormalizerTest {

@DataProvider
public Object[][] primitiveAnyOfVersions() {
return new Object[][] {{"3_0"}, {"3_1"}};
}

@Test(dataProvider = "primitiveAnyOfVersions")
public void testTypedAnyOfPreservesValidationBranchesAndParentConstraints(String version) {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/" + version + "/typescript-axios/primitive-anyof-validation.yaml");
new OpenAPINormalizer(openAPI, Map.of("NORMALIZE_31SPEC", "true")).normalize();
Schema hostname = openAPI.getComponents().getSchemas().get("Hostname");
assertEquals(ModelUtils.getType(hostname), "string");
assertNotEquals(hostname.getNullable(), Boolean.TRUE);
assertEquals(hostname.getAnyOf().size(), 2);
assertEquals(((Schema) hostname.getAnyOf().get(0)).getPattern(), "^[a-z]+[.]example$");
Schema nullable = openAPI.getComponents().getSchemas().get("NullableHostname");
assertEquals(nullable.getNullable(), Boolean.TRUE);
assertEquals(nullable.getAnyOf().size(), 2);
Schema single = openAPI.getComponents().getSchemas().get("SinglePattern");
assertEquals(ModelUtils.getType(single), "string");
assertEquals(single.getMinLength(), Integer.valueOf(2));
assertEquals(single.getAnyOf().size(), 1);
assertEquals(((Schema) single.getAnyOf().get(0)).getPattern(), "^[a-z]+$");
Schema port = openAPI.getComponents().getSchemas().get("Port");
assertEquals(port.getAnyOf().size(), 2);
assertEquals(((Schema) port.getAnyOf().get(0)).getMinimum(), new BigDecimal("1000"));
assertEquals(((Schema) port.getAnyOf().get(1)).getMaximum(), new BigDecimal("9000"));
Schema address = openAPI.getComponents().getSchemas().get("Address");
assertEquals(ModelUtils.getType(address), "object");
assertTrue(address.getRequired().contains("inlineHostname"));
Schema inline = (Schema) address.getProperties().get("inlineHostname");
assertEquals(ModelUtils.getType(inline), "string");
assertNotEquals(inline.getNullable(), Boolean.TRUE);
assertEquals(inline.getAnyOf(), hostname.getAnyOf());
if ("3_1".equals(version)) {
Schema alias = openAPI.getComponents().getSchemas().get("NullableAlias");
assertEquals(alias.getNullable(), Boolean.TRUE);
assertEquals(alias.getAllOf().size(), 1);
assertEquals(((Schema) alias.getAllOf().get(0)).get$ref(), "#/components/schemas/Hostname");
assertEquals(((Schema) address.getProperties().get("nullableAlias")).get$ref(),
"#/components/schemas/NullableAlias");
}
}

@Test
public void testTypedOneOfRetainsValidationOnlyBranches() {
OpenAPINormalizer normalizer = new OpenAPINormalizer(new OpenAPI(), Map.of());
for (List<Schema> branches : List.of(
List.<Schema>of(new Schema().pattern("^[a-z]+$")),
List.<Schema>of(new Schema().pattern("^[a-z]+$"), new Schema().pattern("^[0-9]+$")))) {
Schema schema = new ComposedSchema().type("string").minLength(2)
.oneOf(new ArrayList<>(branches));
Schema normalized = normalizer.normalizeSchema(schema, new HashSet<>());
assertEquals(normalized.getType(), "string");
assertEquals(normalized.getMinLength(), Integer.valueOf(2));
assertEquals(normalized.getOneOf(), branches);
}
}

@Test
public void testTypedAnyOfRetainsUntypedAnnotations() {
OpenAPINormalizer normalizer = new OpenAPINormalizer(new OpenAPI(), Map.of("NORMALIZE_31SPEC", "true"));
for (Schema branch : List.of(new Schema().description("A value"), new Schema().format("hostname"),
new JsonSchema().description("A value"), new JsonSchema().format("hostname"))) {
Schema parent = new ComposedSchema().type("string").minLength(2)
.anyOf(new ArrayList<>(List.of(branch)));
Schema normalized = normalizer.normalizeSchema(parent, new HashSet<>());
assertEquals(normalized.getType(), "string");
assertEquals(normalized.getMinLength(), Integer.valueOf(2));
assertEquals(normalized.getAnyOf().size(), 1);
Schema alternative = (Schema) normalized.getAnyOf().get(0);
assertEquals(alternative.getDescription(), branch.getDescription());
assertEquals(alternative.getFormat(), branch.getFormat());
}
}

@Test
public void testNormalize31PreservesUntypedEnumAndConst() {
OpenAPINormalizer normalizer = new OpenAPINormalizer(new OpenAPI(),
Map.of("NORMALIZE_31SPEC", "true", "SIMPLIFY_ONEOF_ANYOF_ENUM", "false"));
for (Schema branch : List.of(new JsonSchema()._enum(List.of("allowed")),
new JsonSchema()._const("allowed"))) {
Schema parent = new ComposedSchema().type("string")
.anyOf(new ArrayList<>(List.of(branch)));
Schema normalized = normalizer.normalizeSchema(parent, new HashSet<>());
assertEquals(normalized.getType(), "string");
assertNotEquals(normalized.getNullable(), Boolean.TRUE);
assertEquals(normalized.getAnyOf().size(), 1);
Schema alternative = (Schema) normalized.getAnyOf().get(0);
assertEquals(alternative.getEnum(), branch.getEnum());
assertEquals(alternative.getConst(), branch.getConst());
}
}

@Test
public void testNormalize31PreservesContainsWithoutType() {
OpenAPINormalizer normalizer = new OpenAPINormalizer(new OpenAPI(), Map.of("NORMALIZE_31SPEC", "true"));
Schema schema = new JsonSchema().description("At least one matching item")
.contains(new JsonSchema().pattern("^[a-z]+$"));
Schema normalized = normalizer.normalizeSchema(schema, new HashSet<>());
assertNotNull(normalized.getContains());
assertEquals(normalized.getContains().getPattern(), "^[a-z]+$");
}

private static final String SIMPLIFY_ONE_OF_ANY_OF = "SIMPLIFY_ONEOF_ANYOF";
private static final String SIMPLIFY_ONEOF_ANYOF_ENUM = "SIMPLIFY_ONEOF_ANYOF_ENUM";
private static final String REF_AS_PARENT_IN_ALLOF = "REF_AS_PARENT_IN_ALLOF";
Expand Down
Loading