From ee3412a77d27dabfad5a10c6f37729d8e2ec1a34 Mon Sep 17 00:00:00 2001 From: William Cheng Date: Mon, 17 Nov 2025 18:02:43 +0800 Subject: [PATCH 1/2] fix ref sibiling using allOf in normalizer --- .../codegen/OpenAPINormalizer.java | 29 +++++++++++++++++++ .../codegen/OpenAPINormalizerTest.java | 28 ++++++++++++++++-- .../3_1/unsupported_schema_test.yaml | 10 ++++++- 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index 64ed0b624d11..9abf5fae6a6b 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -696,6 +696,11 @@ protected boolean isSelfReference(String name, Schema subSchema) { * @return Schema */ public Schema normalizeSchema(Schema schema, Set visitedSchemas) { + // normalize reference schema + if (StringUtils.isNotEmpty(schema.get$ref())) { + normalizeReferenceSchema(schema); + } + if (skipNormalization(schema, visitedSchemas)) { return schema; } @@ -763,6 +768,30 @@ public Schema normalizeSchema(Schema schema, Set visitedSchemas) { return schema; } + /** + * Normalize reference schema with allOf to support sibling properties + * + * @param schema Schema + */ + protected void normalizeReferenceSchema(Schema schema) { + if (schema.getTitle() != null || schema.getDescription() != null + || schema.getNullable() != null || schema.getDefault() != null || schema.getDeprecated() != null + || schema.getMaximum() != null || schema.getMinimum() != null + || schema.getExclusiveMaximum() != null || schema.getExclusiveMinimum() != null + || schema.getMaxItems() != null || schema.getMinItems() != null + || schema.getMaxProperties() != null || schema.getMinProperties() != null + || schema.getMaxLength() != null || schema.getMinLength() != null + || schema.getWriteOnly() != null || schema.getReadOnly() != null + || schema.getExample() != null || (schema.getExamples() != null && !schema.getExamples().isEmpty()) + || schema.getMultipleOf() != null || schema.getPattern() != null + || (schema.getExtensions() != null && !schema.getExtensions().isEmpty()) + ) { + // create allOf with a $ref schema + schema.addAllOfItem(new Schema<>().$ref(schema.get$ref())); + // clear $ref in original schema + schema.set$ref(null); + } + } /** * Check if normalization is needed. diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java index 118001f798c4..eb85fed62eab 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java @@ -910,7 +910,10 @@ public void testOpenAPINormalizerSimplifyOneOfAnyOf31SpecForIssue18184() { Schema schema2 = openAPI.getComponents().getSchemas().get("Item"); assertEquals(((Schema) schema2.getProperties().get("my_enum")).getAnyOf(), null); - assertEquals(((Schema) schema2.getProperties().get("my_enum")).get$ref(), "#/components/schemas/MyEnum"); + assertEquals(((Schema) schema2.getProperties().get("my_enum")).getAllOf().size(), 1); + assertEquals(((Schema) schema2.getProperties().get("my_enum")).getNullable(), true); + assertEquals(((Schema) schema2.getProperties().get("my_enum")).get$ref(), null); + assertEquals(((Schema) ((Schema) schema2.getProperties().get("my_enum")).getAllOf().get(0)).get$ref(), "#/components/schemas/MyEnum"); } @Test @@ -1104,7 +1107,10 @@ public void testOpenAPINormalizerSimplifyOneOfAnyOf31Spec() { Schema schema18 = openAPI.getComponents().getSchemas().get("OneOfNullAndRef3"); // original oneOf removed and simplified to just $ref (oneOf sub-schema) instead assertEquals(schema18.getOneOf(), null); - assertEquals(schema18.get$ref(), "#/components/schemas/Parent"); + assertEquals(schema18.get$ref(), null); + assertEquals(schema18.getNullable(), true); + assertEquals(((Schema) schema18.getAllOf().get(0)).get$ref(), "#/components/schemas/Parent"); + Schema schema20 = openAPI.getComponents().getSchemas().get("ParentWithOneOfProperty"); assertEquals(((Schema) schema20.getProperties().get("number")).get$ref(), "#/components/schemas/Number"); @@ -1184,6 +1190,24 @@ public void testOpenAPINormalizerProcessingAllOfSchema31Spec() { assertEquals(((Schema) schema2.getProperties().get("property2")).getAllOf(), null); } + @Test + public void testOpenAPINormalizerNormalizeReferenceSchema() { + // to test array schema processing in 3.1 spec + OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_1/unsupported_schema_test.yaml"); + + Schema schema = openAPI.getComponents().getSchemas().get("Dummy"); + assertEquals(((Schema) schema.getProperties().get("property3")).get$ref(), "#/components/schemas/RefSchema"); + + Map inputRules = Map.of("NORMALIZE_31SPEC", "true"); + OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, inputRules); + openAPINormalizer.normalize(); + + Schema schema2 = openAPI.getComponents().getSchemas().get("Dummy"); + assertEquals(((Schema) schema2.getProperties().get("property3")).getAllOf().size(), 1); + assertEquals(((Schema) schema2.getProperties().get("property3")).getDescription(), "Override description in $ref schema"); + assertEquals(((Schema) ((Schema) schema2.getProperties().get("property3")).getAllOf().get(0)).get$ref(), "#/components/schemas/RefSchema"); + } + @Test public void testOpenAPINormalizerComponentsResponses31Spec() { OpenAPI openAPI = TestUtils.parseSpec("src/test/resources/3_1/common-parameters.yaml"); diff --git a/modules/openapi-generator/src/test/resources/3_1/unsupported_schema_test.yaml b/modules/openapi-generator/src/test/resources/3_1/unsupported_schema_test.yaml index 997f8f6a1394..ca11774002af 100644 --- a/modules/openapi-generator/src/test/resources/3_1/unsupported_schema_test.yaml +++ b/modules/openapi-generator/src/test/resources/3_1/unsupported_schema_test.yaml @@ -24,6 +24,11 @@ paths: $ref: "#/components/schemas/Dummy" components: schemas: + RefSchema: + description: reference schema + properties: + id: + type: string Dummy: type: object properties: @@ -61,4 +66,7 @@ components: enum: - FIRST - SECOND - - THIRD \ No newline at end of file + - THIRD + property3: + $ref: "#/components/schemas/RefSchema" + description: Override description in $ref schema \ No newline at end of file From c539772158c6d9106d4d59d70a756aaa83bd3ba7 Mon Sep 17 00:00:00 2001 From: William Cheng Date: Mon, 17 Nov 2025 18:14:09 +0800 Subject: [PATCH 2/2] update samples --- .../encode-decode/build/api/defaultApi.ts | 10 +++---- .../encode-decode/build/apis/DefaultApi.ts | 18 ++++++------- .../encode-decode/build/docs/DefaultApi.md | 27 +++++-------------- .../build/types/ObjectParamAPI.ts | 8 +++--- .../build/types/ObservableAPI.ts | 10 +++---- .../encode-decode/build/types/PromiseAPI.ts | 8 +++--- .../api/openapi.yaml | 4 ++- .../java/okhttp-gson-3.1/api/openapi.yaml | 12 ++++++--- 8 files changed, 45 insertions(+), 52 deletions(-) diff --git a/samples/client/others/typescript-node/encode-decode/build/api/defaultApi.ts b/samples/client/others/typescript-node/encode-decode/build/api/defaultApi.ts index f86121d45b40..ff6d6f883880 100644 --- a/samples/client/others/typescript-node/encode-decode/build/api/defaultApi.ts +++ b/samples/client/others/typescript-node/encode-decode/build/api/defaultApi.ts @@ -334,7 +334,7 @@ export class DefaultApi { /** * */ - public async testDecodeArrayOfNullableObjectsGet (options: {headers: {[name: string]: string}} = {headers: {}}) : Promise<{ response: http.IncomingMessage; body: Array; }> { + public async testDecodeArrayOfNullableObjectsGet (options: {headers: {[name: string]: string}} = {headers: {}}) : Promise<{ response: http.IncomingMessage; body: Array; }> { const localVarPath = this.basePath + '/test/decode/array-of/nullable-objects'; let localVarQueryParameters: any = {}; let localVarHeaderParams: any = (Object).assign({}, this._defaultHeaders); @@ -376,13 +376,13 @@ export class DefaultApi { localVarRequestOptions.form = localVarFormParams; } } - return new Promise<{ response: http.IncomingMessage; body: Array; }>((resolve, reject) => { + return new Promise<{ response: http.IncomingMessage; body: Array; }>((resolve, reject) => { localVarRequest(localVarRequestOptions, (error, response, body) => { if (error) { reject(error); } else { if (response.statusCode && response.statusCode >= 200 && response.statusCode <= 299) { - body = ObjectSerializer.deserialize(body, "Array"); + body = ObjectSerializer.deserialize(body, "Array"); resolve({ response: response, body: body }); } else { reject(new HttpError(response, body, response.statusCode)); @@ -1187,7 +1187,7 @@ export class DefaultApi { * * @param complexObject */ - public async testEncodeArrayOfNullableObjectsPost (complexObject: Array, options: {headers: {[name: string]: string}} = {headers: {}}) : Promise<{ response: http.IncomingMessage; body?: any; }> { + public async testEncodeArrayOfNullableObjectsPost (complexObject: Array, options: {headers: {[name: string]: string}} = {headers: {}}) : Promise<{ response: http.IncomingMessage; body?: any; }> { const localVarPath = this.basePath + '/test/encode/array-of/nullable-objects'; let localVarQueryParameters: any = {}; let localVarHeaderParams: any = (Object).assign({}, this._defaultHeaders); @@ -1209,7 +1209,7 @@ export class DefaultApi { uri: localVarPath, useQuerystring: this._useQuerystring, json: true, - body: ObjectSerializer.serialize(complexObject, "Array") + body: ObjectSerializer.serialize(complexObject, "Array") }; let authenticationPromise = Promise.resolve(); diff --git a/samples/client/others/typescript/encode-decode/build/apis/DefaultApi.ts b/samples/client/others/typescript/encode-decode/build/apis/DefaultApi.ts index 8fa7642594ae..277ac5827534 100644 --- a/samples/client/others/typescript/encode-decode/build/apis/DefaultApi.ts +++ b/samples/client/others/typescript/encode-decode/build/apis/DefaultApi.ts @@ -453,7 +453,7 @@ export class DefaultApiRequestFactory extends BaseAPIRequestFactory { /** * @param complexObject */ - public async testEncodeArrayOfNullableObjectsPost(complexObject: Array, _options?: Configuration): Promise { + public async testEncodeArrayOfNullableObjectsPost(complexObject: Array, _options?: Configuration): Promise { let _config = _options || this.configuration; // verify required parameter 'complexObject' is not null or undefined @@ -476,7 +476,7 @@ export class DefaultApiRequestFactory extends BaseAPIRequestFactory { ]); requestContext.setHeaderParam("Content-Type", contentType); const serializedBody = ObjectSerializer.stringify( - ObjectSerializer.serialize(complexObject, "Array", ""), + ObjectSerializer.serialize(complexObject, "Array", ""), contentType ); requestContext.setBody(serializedBody); @@ -1127,22 +1127,22 @@ export class DefaultApiResponseProcessor { * @params response Response returned by the server for a request to testDecodeArrayOfNullableObjectsGet * @throws ApiException if the response code was not in [200, 299] */ - public async testDecodeArrayOfNullableObjectsGetWithHttpInfo(response: ResponseContext): Promise >> { + public async testDecodeArrayOfNullableObjectsGetWithHttpInfo(response: ResponseContext): Promise >> { const contentType = ObjectSerializer.normalizeMediaType(response.headers["content-type"]); if (isCodeInRange("200", response.httpStatusCode)) { - const body: Array = ObjectSerializer.deserialize( + const body: Array = ObjectSerializer.deserialize( ObjectSerializer.parse(await response.body.text(), contentType), - "Array", "" - ) as Array; + "Array", "" + ) as Array; return new HttpInfo(response.httpStatusCode, response.headers, response.body, body); } // Work around for missing responses in specification, e.g. for petstore.yaml if (response.httpStatusCode >= 200 && response.httpStatusCode <= 299) { - const body: Array = ObjectSerializer.deserialize( + const body: Array = ObjectSerializer.deserialize( ObjectSerializer.parse(await response.body.text(), contentType), - "Array", "" - ) as Array; + "Array", "" + ) as Array; return new HttpInfo(response.httpStatusCode, response.headers, response.body, body); } diff --git a/samples/client/others/typescript/encode-decode/build/docs/DefaultApi.md b/samples/client/others/typescript/encode-decode/build/docs/DefaultApi.md index 8645fb8ea21f..5602bc6203fc 100644 --- a/samples/client/others/typescript/encode-decode/build/docs/DefaultApi.md +++ b/samples/client/others/typescript/encode-decode/build/docs/DefaultApi.md @@ -219,7 +219,7 @@ No authorization required [[Back to top]](#) [[Back to API list]](README.md#documentation-for-api-endpoints) [[Back to Model list]](README.md#documentation-for-models) [[Back to README]](README.md) # **testDecodeArrayOfNullableObjectsGet** -> Array testDecodeArrayOfNullableObjectsGet() +> Array testDecodeArrayOfNullableObjectsGet() ### Example @@ -244,7 +244,7 @@ This endpoint does not need any parameter. ### Return type -**Array** +**Array** ### Authorization @@ -892,12 +892,7 @@ const apiInstance = new DefaultApi(configuration); const request: DefaultApiTestEncodeArrayOfNullableObjectsPostRequest = { complexObject: [ - { - requiredProperty: "requiredProperty_example", - requiredNullableProperty: "requiredNullableProperty_example", - optionalProperty: "optionalProperty_example", - optionalNullableProperty: "optionalNullableProperty_example", - }, + null, ], }; @@ -910,7 +905,7 @@ console.log('API called successfully. Returned data:', data); Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - **complexObject** | **Array**| | + **complexObject** | **Array**| | ### Return type @@ -1059,12 +1054,7 @@ const apiInstance = new DefaultApi(configuration); const request: DefaultApiTestEncodeCompositeObjectsPostRequest = { compositeObject: { - optionalNullableInnerObject: { - requiredProperty: "requiredProperty_example", - requiredNullableProperty: "requiredNullableProperty_example", - optionalProperty: "optionalProperty_example", - optionalNullableProperty: "optionalNullableProperty_example", - }, + optionalNullableInnerObject: null, }, }; @@ -1179,12 +1169,7 @@ const apiInstance = new DefaultApi(configuration); const request: DefaultApiTestEncodeMapOfObjectsPostRequest = { requestBody: { - "key": { - requiredProperty: "requiredProperty_example", - requiredNullableProperty: "requiredNullableProperty_example", - optionalProperty: "optionalProperty_example", - optionalNullableProperty: "optionalNullableProperty_example", - }, + "key": null, }, }; diff --git a/samples/client/others/typescript/encode-decode/build/types/ObjectParamAPI.ts b/samples/client/others/typescript/encode-decode/build/types/ObjectParamAPI.ts index d7263e3d719b..2ff95a738dbf 100644 --- a/samples/client/others/typescript/encode-decode/build/types/ObjectParamAPI.ts +++ b/samples/client/others/typescript/encode-decode/build/types/ObjectParamAPI.ts @@ -77,10 +77,10 @@ export interface DefaultApiTestEncodeArrayOfMapsOfObjectsPostRequest { export interface DefaultApiTestEncodeArrayOfNullableObjectsPostRequest { /** * - * @type Array<ComplexObject> + * @type Array<ComplexObject | null> * @memberof DefaultApitestEncodeArrayOfNullableObjectsPost */ - complexObject: Array + complexObject: Array } export interface DefaultApiTestEncodeArrayOfNullablePostRequest { @@ -266,14 +266,14 @@ export class ObjectDefaultApi { /** * @param param the request object */ - public testDecodeArrayOfNullableObjectsGetWithHttpInfo(param: DefaultApiTestDecodeArrayOfNullableObjectsGetRequest = {}, options?: ConfigurationOptions): Promise>> { + public testDecodeArrayOfNullableObjectsGetWithHttpInfo(param: DefaultApiTestDecodeArrayOfNullableObjectsGetRequest = {}, options?: ConfigurationOptions): Promise>> { return this.api.testDecodeArrayOfNullableObjectsGetWithHttpInfo( options).toPromise(); } /** * @param param the request object */ - public testDecodeArrayOfNullableObjectsGet(param: DefaultApiTestDecodeArrayOfNullableObjectsGetRequest = {}, options?: ConfigurationOptions): Promise> { + public testDecodeArrayOfNullableObjectsGet(param: DefaultApiTestDecodeArrayOfNullableObjectsGetRequest = {}, options?: ConfigurationOptions): Promise> { return this.api.testDecodeArrayOfNullableObjectsGet( options).toPromise(); } diff --git a/samples/client/others/typescript/encode-decode/build/types/ObservableAPI.ts b/samples/client/others/typescript/encode-decode/build/types/ObservableAPI.ts index 98a4d0cbf06f..2adba195dbb9 100644 --- a/samples/client/others/typescript/encode-decode/build/types/ObservableAPI.ts +++ b/samples/client/others/typescript/encode-decode/build/types/ObservableAPI.ts @@ -136,7 +136,7 @@ export class ObservableDefaultApi { /** */ - public testDecodeArrayOfNullableObjectsGetWithHttpInfo(_options?: ConfigurationOptions): Observable>> { + public testDecodeArrayOfNullableObjectsGetWithHttpInfo(_options?: ConfigurationOptions): Observable>> { const _config = mergeConfiguration(this.configuration, _options); const requestContextPromise = this.requestFactory.testDecodeArrayOfNullableObjectsGet(_config); @@ -158,8 +158,8 @@ export class ObservableDefaultApi { /** */ - public testDecodeArrayOfNullableObjectsGet(_options?: ConfigurationOptions): Observable> { - return this.testDecodeArrayOfNullableObjectsGetWithHttpInfo(_options).pipe(map((apiResponse: HttpInfo>) => apiResponse.data)); + public testDecodeArrayOfNullableObjectsGet(_options?: ConfigurationOptions): Observable> { + return this.testDecodeArrayOfNullableObjectsGetWithHttpInfo(_options).pipe(map((apiResponse: HttpInfo>) => apiResponse.data)); } /** @@ -533,7 +533,7 @@ export class ObservableDefaultApi { /** * @param complexObject */ - public testEncodeArrayOfNullableObjectsPostWithHttpInfo(complexObject: Array, _options?: ConfigurationOptions): Observable> { + public testEncodeArrayOfNullableObjectsPostWithHttpInfo(complexObject: Array, _options?: ConfigurationOptions): Observable> { const _config = mergeConfiguration(this.configuration, _options); const requestContextPromise = this.requestFactory.testEncodeArrayOfNullableObjectsPost(complexObject, _config); @@ -556,7 +556,7 @@ export class ObservableDefaultApi { /** * @param complexObject */ - public testEncodeArrayOfNullableObjectsPost(complexObject: Array, _options?: ConfigurationOptions): Observable { + public testEncodeArrayOfNullableObjectsPost(complexObject: Array, _options?: ConfigurationOptions): Observable { return this.testEncodeArrayOfNullableObjectsPostWithHttpInfo(complexObject, _options).pipe(map((apiResponse: HttpInfo) => apiResponse.data)); } diff --git a/samples/client/others/typescript/encode-decode/build/types/PromiseAPI.ts b/samples/client/others/typescript/encode-decode/build/types/PromiseAPI.ts index 319ad2d893fd..a385e1a69e3d 100644 --- a/samples/client/others/typescript/encode-decode/build/types/PromiseAPI.ts +++ b/samples/client/others/typescript/encode-decode/build/types/PromiseAPI.ts @@ -84,7 +84,7 @@ export class PromiseDefaultApi { /** */ - public testDecodeArrayOfNullableObjectsGetWithHttpInfo(_options?: PromiseConfigurationOptions): Promise>> { + public testDecodeArrayOfNullableObjectsGetWithHttpInfo(_options?: PromiseConfigurationOptions): Promise>> { const observableOptions = wrapOptions(_options); const result = this.api.testDecodeArrayOfNullableObjectsGetWithHttpInfo(observableOptions); return result.toPromise(); @@ -92,7 +92,7 @@ export class PromiseDefaultApi { /** */ - public testDecodeArrayOfNullableObjectsGet(_options?: PromiseConfigurationOptions): Promise> { + public testDecodeArrayOfNullableObjectsGet(_options?: PromiseConfigurationOptions): Promise> { const observableOptions = wrapOptions(_options); const result = this.api.testDecodeArrayOfNullableObjectsGet(observableOptions); return result.toPromise(); @@ -313,7 +313,7 @@ export class PromiseDefaultApi { /** * @param complexObject */ - public testEncodeArrayOfNullableObjectsPostWithHttpInfo(complexObject: Array, _options?: PromiseConfigurationOptions): Promise> { + public testEncodeArrayOfNullableObjectsPostWithHttpInfo(complexObject: Array, _options?: PromiseConfigurationOptions): Promise> { const observableOptions = wrapOptions(_options); const result = this.api.testEncodeArrayOfNullableObjectsPostWithHttpInfo(complexObject, observableOptions); return result.toPromise(); @@ -322,7 +322,7 @@ export class PromiseDefaultApi { /** * @param complexObject */ - public testEncodeArrayOfNullableObjectsPost(complexObject: Array, _options?: PromiseConfigurationOptions): Promise { + public testEncodeArrayOfNullableObjectsPost(complexObject: Array, _options?: PromiseConfigurationOptions): Promise { const observableOptions = wrapOptions(_options); const result = this.api.testEncodeArrayOfNullableObjectsPost(complexObject, observableOptions); return result.toPromise(); diff --git a/samples/client/petstore/java/okhttp-gson-3.1-duplicated-operationid/api/openapi.yaml b/samples/client/petstore/java/okhttp-gson-3.1-duplicated-operationid/api/openapi.yaml index e6f1d06f1db8..d0c825c5621a 100644 --- a/samples/client/petstore/java/okhttp-gson-3.1-duplicated-operationid/api/openapi.yaml +++ b/samples/client/petstore/java/okhttp-gson-3.1-duplicated-operationid/api/openapi.yaml @@ -64,7 +64,9 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/myObject" + allOf: + - $ref: "#/components/schemas/myObject" + nullable: true description: "" tags: - fake diff --git a/samples/client/petstore/java/okhttp-gson-3.1/api/openapi.yaml b/samples/client/petstore/java/okhttp-gson-3.1/api/openapi.yaml index 5c7d496f5756..9d15c38d5d60 100644 --- a/samples/client/petstore/java/okhttp-gson-3.1/api/openapi.yaml +++ b/samples/client/petstore/java/okhttp-gson-3.1/api/openapi.yaml @@ -786,7 +786,9 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/myObject" + allOf: + - $ref: "#/components/schemas/myObject" + nullable: true description: "" tags: - fake @@ -799,7 +801,9 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/myObject" + allOf: + - $ref: "#/components/schemas/myObject" + nullable: true description: "" tags: - fake @@ -813,7 +817,9 @@ paths: application/json: schema: items: - $ref: "#/components/schemas/myObject" + allOf: + - $ref: "#/components/schemas/myObject" + nullable: true nullable: true type: array description: ""