Fix siblings of $ref using allOf in openapi normalizer (#22364)

* fix ref sibiling using allOf in normalizer

* update samples
This commit is contained in:
William Cheng
2025-11-19 20:35:07 +08:00
committed by GitHub
parent 6699ecd9d2
commit 554e10dc34
11 changed files with 109 additions and 55 deletions

View File

@@ -696,6 +696,11 @@ public class OpenAPINormalizer {
* @return Schema
*/
public Schema normalizeSchema(Schema schema, Set<Schema> visitedSchemas) {
// normalize reference schema
if (StringUtils.isNotEmpty(schema.get$ref())) {
normalizeReferenceSchema(schema);
}
if (skipNormalization(schema, visitedSchemas)) {
return schema;
}
@@ -763,6 +768,30 @@ public class OpenAPINormalizer {
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.

View File

@@ -910,7 +910,10 @@ public class OpenAPINormalizerTest {
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 class OpenAPINormalizerTest {
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 class OpenAPINormalizerTest {
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<String, String> 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");

View File

@@ -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
- THIRD
property3:
$ref: "#/components/schemas/RefSchema"
description: Override description in $ref schema

View File

@@ -334,7 +334,7 @@ export class DefaultApi {
/**
*
*/
public async testDecodeArrayOfNullableObjectsGet (options: {headers: {[name: string]: string}} = {headers: {}}) : Promise<{ response: http.IncomingMessage; body: Array<ComplexObject>; }> {
public async testDecodeArrayOfNullableObjectsGet (options: {headers: {[name: string]: string}} = {headers: {}}) : Promise<{ response: http.IncomingMessage; body: Array<ComplexObject | null>; }> {
const localVarPath = this.basePath + '/test/decode/array-of/nullable-objects';
let localVarQueryParameters: any = {};
let localVarHeaderParams: any = (<any>Object).assign({}, this._defaultHeaders);
@@ -376,13 +376,13 @@ export class DefaultApi {
localVarRequestOptions.form = localVarFormParams;
}
}
return new Promise<{ response: http.IncomingMessage; body: Array<ComplexObject>; }>((resolve, reject) => {
return new Promise<{ response: http.IncomingMessage; body: Array<ComplexObject | null>; }>((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<ComplexObject>");
body = ObjectSerializer.deserialize(body, "Array<ComplexObject | null>");
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<ComplexObject>, options: {headers: {[name: string]: string}} = {headers: {}}) : Promise<{ response: http.IncomingMessage; body?: any; }> {
public async testEncodeArrayOfNullableObjectsPost (complexObject: Array<ComplexObject | null>, 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 = (<any>Object).assign({}, this._defaultHeaders);
@@ -1209,7 +1209,7 @@ export class DefaultApi {
uri: localVarPath,
useQuerystring: this._useQuerystring,
json: true,
body: ObjectSerializer.serialize(complexObject, "Array<ComplexObject>")
body: ObjectSerializer.serialize(complexObject, "Array<ComplexObject | null>")
};
let authenticationPromise = Promise.resolve();

View File

@@ -453,7 +453,7 @@ export class DefaultApiRequestFactory extends BaseAPIRequestFactory {
/**
* @param complexObject
*/
public async testEncodeArrayOfNullableObjectsPost(complexObject: Array<ComplexObject>, _options?: Configuration): Promise<RequestContext> {
public async testEncodeArrayOfNullableObjectsPost(complexObject: Array<ComplexObject | null>, _options?: Configuration): Promise<RequestContext> {
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<ComplexObject>", ""),
ObjectSerializer.serialize(complexObject, "Array<ComplexObject | null>", ""),
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<HttpInfo<Array<ComplexObject> >> {
public async testDecodeArrayOfNullableObjectsGetWithHttpInfo(response: ResponseContext): Promise<HttpInfo<Array<ComplexObject | null> >> {
const contentType = ObjectSerializer.normalizeMediaType(response.headers["content-type"]);
if (isCodeInRange("200", response.httpStatusCode)) {
const body: Array<ComplexObject> = ObjectSerializer.deserialize(
const body: Array<ComplexObject | null> = ObjectSerializer.deserialize(
ObjectSerializer.parse(await response.body.text(), contentType),
"Array<ComplexObject>", ""
) as Array<ComplexObject>;
"Array<ComplexObject | null>", ""
) as Array<ComplexObject | null>;
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<ComplexObject> = ObjectSerializer.deserialize(
const body: Array<ComplexObject | null> = ObjectSerializer.deserialize(
ObjectSerializer.parse(await response.body.text(), contentType),
"Array<ComplexObject>", ""
) as Array<ComplexObject>;
"Array<ComplexObject | null>", ""
) as Array<ComplexObject | null>;
return new HttpInfo(response.httpStatusCode, response.headers, response.body, body);
}

View File

@@ -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<ComplexObject> testDecodeArrayOfNullableObjectsGet()
> Array<ComplexObject | null> testDecodeArrayOfNullableObjectsGet()
### Example
@@ -244,7 +244,7 @@ This endpoint does not need any parameter.
### Return type
**Array<ComplexObject>**
**Array<ComplexObject | null>**
### 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>**| |
**complexObject** | **Array<ComplexObject | null>**| |
### 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,
},
};

View File

@@ -77,10 +77,10 @@ export interface DefaultApiTestEncodeArrayOfMapsOfObjectsPostRequest {
export interface DefaultApiTestEncodeArrayOfNullableObjectsPostRequest {
/**
*
* @type Array&lt;ComplexObject&gt;
* @type Array&lt;ComplexObject | null&gt;
* @memberof DefaultApitestEncodeArrayOfNullableObjectsPost
*/
complexObject: Array<ComplexObject>
complexObject: Array<ComplexObject | null>
}
export interface DefaultApiTestEncodeArrayOfNullablePostRequest {
@@ -266,14 +266,14 @@ export class ObjectDefaultApi {
/**
* @param param the request object
*/
public testDecodeArrayOfNullableObjectsGetWithHttpInfo(param: DefaultApiTestDecodeArrayOfNullableObjectsGetRequest = {}, options?: ConfigurationOptions): Promise<HttpInfo<Array<ComplexObject>>> {
public testDecodeArrayOfNullableObjectsGetWithHttpInfo(param: DefaultApiTestDecodeArrayOfNullableObjectsGetRequest = {}, options?: ConfigurationOptions): Promise<HttpInfo<Array<ComplexObject | null>>> {
return this.api.testDecodeArrayOfNullableObjectsGetWithHttpInfo( options).toPromise();
}
/**
* @param param the request object
*/
public testDecodeArrayOfNullableObjectsGet(param: DefaultApiTestDecodeArrayOfNullableObjectsGetRequest = {}, options?: ConfigurationOptions): Promise<Array<ComplexObject>> {
public testDecodeArrayOfNullableObjectsGet(param: DefaultApiTestDecodeArrayOfNullableObjectsGetRequest = {}, options?: ConfigurationOptions): Promise<Array<ComplexObject | null>> {
return this.api.testDecodeArrayOfNullableObjectsGet( options).toPromise();
}

View File

@@ -136,7 +136,7 @@ export class ObservableDefaultApi {
/**
*/
public testDecodeArrayOfNullableObjectsGetWithHttpInfo(_options?: ConfigurationOptions): Observable<HttpInfo<Array<ComplexObject>>> {
public testDecodeArrayOfNullableObjectsGetWithHttpInfo(_options?: ConfigurationOptions): Observable<HttpInfo<Array<ComplexObject | null>>> {
const _config = mergeConfiguration(this.configuration, _options);
const requestContextPromise = this.requestFactory.testDecodeArrayOfNullableObjectsGet(_config);
@@ -158,8 +158,8 @@ export class ObservableDefaultApi {
/**
*/
public testDecodeArrayOfNullableObjectsGet(_options?: ConfigurationOptions): Observable<Array<ComplexObject>> {
return this.testDecodeArrayOfNullableObjectsGetWithHttpInfo(_options).pipe(map((apiResponse: HttpInfo<Array<ComplexObject>>) => apiResponse.data));
public testDecodeArrayOfNullableObjectsGet(_options?: ConfigurationOptions): Observable<Array<ComplexObject | null>> {
return this.testDecodeArrayOfNullableObjectsGetWithHttpInfo(_options).pipe(map((apiResponse: HttpInfo<Array<ComplexObject | null>>) => apiResponse.data));
}
/**
@@ -533,7 +533,7 @@ export class ObservableDefaultApi {
/**
* @param complexObject
*/
public testEncodeArrayOfNullableObjectsPostWithHttpInfo(complexObject: Array<ComplexObject>, _options?: ConfigurationOptions): Observable<HttpInfo<void>> {
public testEncodeArrayOfNullableObjectsPostWithHttpInfo(complexObject: Array<ComplexObject | null>, _options?: ConfigurationOptions): Observable<HttpInfo<void>> {
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<ComplexObject>, _options?: ConfigurationOptions): Observable<void> {
public testEncodeArrayOfNullableObjectsPost(complexObject: Array<ComplexObject | null>, _options?: ConfigurationOptions): Observable<void> {
return this.testEncodeArrayOfNullableObjectsPostWithHttpInfo(complexObject, _options).pipe(map((apiResponse: HttpInfo<void>) => apiResponse.data));
}

View File

@@ -84,7 +84,7 @@ export class PromiseDefaultApi {
/**
*/
public testDecodeArrayOfNullableObjectsGetWithHttpInfo(_options?: PromiseConfigurationOptions): Promise<HttpInfo<Array<ComplexObject>>> {
public testDecodeArrayOfNullableObjectsGetWithHttpInfo(_options?: PromiseConfigurationOptions): Promise<HttpInfo<Array<ComplexObject | null>>> {
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<Array<ComplexObject>> {
public testDecodeArrayOfNullableObjectsGet(_options?: PromiseConfigurationOptions): Promise<Array<ComplexObject | null>> {
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<ComplexObject>, _options?: PromiseConfigurationOptions): Promise<HttpInfo<void>> {
public testEncodeArrayOfNullableObjectsPostWithHttpInfo(complexObject: Array<ComplexObject | null>, _options?: PromiseConfigurationOptions): Promise<HttpInfo<void>> {
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<ComplexObject>, _options?: PromiseConfigurationOptions): Promise<void> {
public testEncodeArrayOfNullableObjectsPost(complexObject: Array<ComplexObject | null>, _options?: PromiseConfigurationOptions): Promise<void> {
const observableOptions = wrapOptions(_options);
const result = this.api.testEncodeArrayOfNullableObjectsPost(complexObject, observableOptions);
return result.toPromise();

View File

@@ -64,7 +64,9 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/myObject"
allOf:
- $ref: "#/components/schemas/myObject"
nullable: true
description: ""
tags:
- fake

View File

@@ -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: ""