From c03e41ae9843786d763345793c05ddf9fae1f062 Mon Sep 17 00:00:00 2001 From: Tristan <38611461+angelaki@users.noreply.github.com> Date: Wed, 23 Sep 2026 15:41:06 +0200 Subject: [PATCH 1/3] fix(typescript-angular): serialize nested deepObject query values (#24983) Preserve nested property paths using recursive bracket notation and indexed arrays. Document the OpenAPI extension and cover encoding, dates, nullish values and existing query styles in the Angular sample tests. --- .../typescript-angular/README.mustache | 10 +++++ .../api.base.service.mustache | 26 +++++++++++- .../builds/query-param-deep-object/README.md | 10 +++++ .../api.base.service.ts | 26 +++++++++++- .../builds/query-param-form/README.md | 10 +++++ .../query-param-form/api.base.service.ts | 26 +++++++++++- .../builds/query-param-json/README.md | 10 +++++ .../query-param-json/api.base.service.ts | 26 +++++++++++- .../src/api.query_param_deep_object.spec.ts | 42 ++++++++++++++++++- .../composed-schemas-tagged-unions/README.md | 10 +++++ .../api.base.service.ts | 26 +++++++++++- .../builds/composed-schemas/README.md | 10 +++++ .../composed-schemas/api.base.service.ts | 26 +++++++++++- .../builds/default/README.md | 10 +++++ .../builds/default/api.base.service.ts | 26 +++++++++++- .../builds/default/README.md | 10 +++++ .../builds/default/api.base.service.ts | 26 +++++++++++- .../builds/default/README.md | 10 +++++ .../builds/default/api.base.service.ts | 26 +++++++++++- .../builds/default/README.md | 10 +++++ .../builds/default/api.base.service.ts | 26 +++++++++++- .../builds/default/README.md | 10 +++++ .../builds/default/api.base.service.ts | 26 +++++++++++- .../builds/default/README.md | 10 +++++ .../builds/default/api.base.service.ts | 26 +++++++++++- .../builds/default/README.md | 10 +++++ .../builds/default/api.base.service.ts | 26 +++++++++++- .../builds/default/README.md | 10 +++++ .../builds/default/api.base.service.ts | 26 +++++++++++- .../builds/default/README.md | 10 +++++ .../builds/default/api.base.service.ts | 26 +++++++++++- .../builds/default/README.md | 10 +++++ .../builds/default/api.base.service.ts | 26 +++++++++++- 33 files changed, 601 insertions(+), 17 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/typescript-angular/README.mustache b/modules/openapi-generator/src/main/resources/typescript-angular/README.mustache index afe430da51ce..d235874d6925 100644 --- a/modules/openapi-generator/src/main/resources/typescript-angular/README.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-angular/README.mustache @@ -183,3 +183,13 @@ new Configuration({ [parameter-locations-url]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#parameter-locations [style-values-url]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#style-values [@honoluluhenk/http-param-expander]: https://www.npmjs.com/package/@honoluluhenk/http-param-expander + +## Deep-object query parameters + +For `style: deepObject`, nested objects are serialized using bracket notation, for example +`filter[name][contains]=Alice`. Arrays and sets use zero-based indices, for example +`filter[sort][0][field]=name`. Dates are serialized as ISO strings; null and undefined +values and empty containers are omitted. + +OpenAPI only specifies deepObject serialization for flat objects. This recursive +encoding is a generator extension and requires a server that accepts bracket notation. diff --git a/modules/openapi-generator/src/main/resources/typescript-angular/api.base.service.mustache b/modules/openapi-generator/src/main/resources/typescript-angular/api.base.service.mustache index ed7a92be8bd2..f994b120f0c0 100644 --- a/modules/openapi-generator/src/main/resources/typescript-angular/api.base.service.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-angular/api.base.service.mustache @@ -41,7 +41,7 @@ export class BaseService { } return Object.keys(value as Record).reduce( - (hp, k) => hp.append(`${key}[${k}]`, value[k]), + (hp, k) => this.addToHttpParamsDeepObject(hp, `${key}[${k}]`, value[k]), httpParams, ); } else if (paramStyle === QueryParamStyle.Json) { @@ -85,4 +85,28 @@ export class BaseService { } } } + + /** + * Nested deepObject values are an extension to OpenAPI's flat-object serialization. + * Object properties use bracket notation; array and set elements use numeric indices. + */ + private addToHttpParamsDeepObject(httpParams: OpenApiHttpParams, key: string, value: any): OpenApiHttpParams { + if (value === null || value === undefined) { + return httpParams; + } else if (value instanceof Date) { + return httpParams.append(key, value.toISOString()); + } else if (Array.isArray(value) || value instanceof Set) { + const array = Array.isArray(value) ? value : Array.from(value); + return array.reduce( + (hp, item, index) => this.addToHttpParamsDeepObject(hp, `${key}[${index}]`, item), + httpParams, + ); + } else if (typeof value === 'object') { + return Object.keys(value).reduce( + (hp, k) => this.addToHttpParamsDeepObject(hp, `${key}[${k}]`, value[k]), + httpParams, + ); + } + return httpParams.append(key, value.toString()); + } } diff --git a/samples/client/others/typescript-angular-v20/builds/query-param-deep-object/README.md b/samples/client/others/typescript-angular-v20/builds/query-param-deep-object/README.md index 36cdfe6172a4..d1dfc4270d3e 100644 --- a/samples/client/others/typescript-angular-v20/builds/query-param-deep-object/README.md +++ b/samples/client/others/typescript-angular-v20/builds/query-param-deep-object/README.md @@ -183,3 +183,13 @@ new Configuration({ [parameter-locations-url]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#parameter-locations [style-values-url]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#style-values [@honoluluhenk/http-param-expander]: https://www.npmjs.com/package/@honoluluhenk/http-param-expander + +## Deep-object query parameters + +For `style: deepObject`, nested objects are serialized using bracket notation, for example +`filter[name][contains]=Alice`. Arrays and sets use zero-based indices, for example +`filter[sort][0][field]=name`. Dates are serialized as ISO strings; null and undefined +values and empty containers are omitted. + +OpenAPI only specifies deepObject serialization for flat objects. This recursive +encoding is a generator extension and requires a server that accepts bracket notation. diff --git a/samples/client/others/typescript-angular-v20/builds/query-param-deep-object/api.base.service.ts b/samples/client/others/typescript-angular-v20/builds/query-param-deep-object/api.base.service.ts index 1b63f4457d9c..6999fb69b2e5 100644 --- a/samples/client/others/typescript-angular-v20/builds/query-param-deep-object/api.base.service.ts +++ b/samples/client/others/typescript-angular-v20/builds/query-param-deep-object/api.base.service.ts @@ -49,7 +49,7 @@ export class BaseService { } return Object.keys(value as Record).reduce( - (hp, k) => hp.append(`${key}[${k}]`, value[k]), + (hp, k) => this.addToHttpParamsDeepObject(hp, `${key}[${k}]`, value[k]), httpParams, ); } else if (paramStyle === QueryParamStyle.Json) { @@ -93,4 +93,28 @@ export class BaseService { } } } + + /** + * Nested deepObject values are an extension to OpenAPI's flat-object serialization. + * Object properties use bracket notation; array and set elements use numeric indices. + */ + private addToHttpParamsDeepObject(httpParams: OpenApiHttpParams, key: string, value: any): OpenApiHttpParams { + if (value === null || value === undefined) { + return httpParams; + } else if (value instanceof Date) { + return httpParams.append(key, value.toISOString()); + } else if (Array.isArray(value) || value instanceof Set) { + const array = Array.isArray(value) ? value : Array.from(value); + return array.reduce( + (hp, item, index) => this.addToHttpParamsDeepObject(hp, `${key}[${index}]`, item), + httpParams, + ); + } else if (typeof value === 'object') { + return Object.keys(value).reduce( + (hp, k) => this.addToHttpParamsDeepObject(hp, `${key}[${k}]`, value[k]), + httpParams, + ); + } + return httpParams.append(key, value.toString()); + } } diff --git a/samples/client/others/typescript-angular-v20/builds/query-param-form/README.md b/samples/client/others/typescript-angular-v20/builds/query-param-form/README.md index f22bb53bfd05..d4994fd9483a 100644 --- a/samples/client/others/typescript-angular-v20/builds/query-param-form/README.md +++ b/samples/client/others/typescript-angular-v20/builds/query-param-form/README.md @@ -183,3 +183,13 @@ new Configuration({ [parameter-locations-url]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#parameter-locations [style-values-url]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#style-values [@honoluluhenk/http-param-expander]: https://www.npmjs.com/package/@honoluluhenk/http-param-expander + +## Deep-object query parameters + +For `style: deepObject`, nested objects are serialized using bracket notation, for example +`filter[name][contains]=Alice`. Arrays and sets use zero-based indices, for example +`filter[sort][0][field]=name`. Dates are serialized as ISO strings; null and undefined +values and empty containers are omitted. + +OpenAPI only specifies deepObject serialization for flat objects. This recursive +encoding is a generator extension and requires a server that accepts bracket notation. diff --git a/samples/client/others/typescript-angular-v20/builds/query-param-form/api.base.service.ts b/samples/client/others/typescript-angular-v20/builds/query-param-form/api.base.service.ts index 7801405134b7..312f99d4c190 100644 --- a/samples/client/others/typescript-angular-v20/builds/query-param-form/api.base.service.ts +++ b/samples/client/others/typescript-angular-v20/builds/query-param-form/api.base.service.ts @@ -49,7 +49,7 @@ export class BaseService { } return Object.keys(value as Record).reduce( - (hp, k) => hp.append(`${key}[${k}]`, value[k]), + (hp, k) => this.addToHttpParamsDeepObject(hp, `${key}[${k}]`, value[k]), httpParams, ); } else if (paramStyle === QueryParamStyle.Json) { @@ -93,4 +93,28 @@ export class BaseService { } } } + + /** + * Nested deepObject values are an extension to OpenAPI's flat-object serialization. + * Object properties use bracket notation; array and set elements use numeric indices. + */ + private addToHttpParamsDeepObject(httpParams: OpenApiHttpParams, key: string, value: any): OpenApiHttpParams { + if (value === null || value === undefined) { + return httpParams; + } else if (value instanceof Date) { + return httpParams.append(key, value.toISOString()); + } else if (Array.isArray(value) || value instanceof Set) { + const array = Array.isArray(value) ? value : Array.from(value); + return array.reduce( + (hp, item, index) => this.addToHttpParamsDeepObject(hp, `${key}[${index}]`, item), + httpParams, + ); + } else if (typeof value === 'object') { + return Object.keys(value).reduce( + (hp, k) => this.addToHttpParamsDeepObject(hp, `${key}[${k}]`, value[k]), + httpParams, + ); + } + return httpParams.append(key, value.toString()); + } } diff --git a/samples/client/others/typescript-angular-v20/builds/query-param-json/README.md b/samples/client/others/typescript-angular-v20/builds/query-param-json/README.md index b6edb64ef1f8..ef4b8077e3d8 100644 --- a/samples/client/others/typescript-angular-v20/builds/query-param-json/README.md +++ b/samples/client/others/typescript-angular-v20/builds/query-param-json/README.md @@ -183,3 +183,13 @@ new Configuration({ [parameter-locations-url]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#parameter-locations [style-values-url]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#style-values [@honoluluhenk/http-param-expander]: https://www.npmjs.com/package/@honoluluhenk/http-param-expander + +## Deep-object query parameters + +For `style: deepObject`, nested objects are serialized using bracket notation, for example +`filter[name][contains]=Alice`. Arrays and sets use zero-based indices, for example +`filter[sort][0][field]=name`. Dates are serialized as ISO strings; null and undefined +values and empty containers are omitted. + +OpenAPI only specifies deepObject serialization for flat objects. This recursive +encoding is a generator extension and requires a server that accepts bracket notation. diff --git a/samples/client/others/typescript-angular-v20/builds/query-param-json/api.base.service.ts b/samples/client/others/typescript-angular-v20/builds/query-param-json/api.base.service.ts index 56df5e8f84b6..7fe22193a2c1 100644 --- a/samples/client/others/typescript-angular-v20/builds/query-param-json/api.base.service.ts +++ b/samples/client/others/typescript-angular-v20/builds/query-param-json/api.base.service.ts @@ -49,7 +49,7 @@ export class BaseService { } return Object.keys(value as Record).reduce( - (hp, k) => hp.append(`${key}[${k}]`, value[k]), + (hp, k) => this.addToHttpParamsDeepObject(hp, `${key}[${k}]`, value[k]), httpParams, ); } else if (paramStyle === QueryParamStyle.Json) { @@ -93,4 +93,28 @@ export class BaseService { } } } + + /** + * Nested deepObject values are an extension to OpenAPI's flat-object serialization. + * Object properties use bracket notation; array and set elements use numeric indices. + */ + private addToHttpParamsDeepObject(httpParams: OpenApiHttpParams, key: string, value: any): OpenApiHttpParams { + if (value === null || value === undefined) { + return httpParams; + } else if (value instanceof Date) { + return httpParams.append(key, value.toISOString()); + } else if (Array.isArray(value) || value instanceof Set) { + const array = Array.isArray(value) ? value : Array.from(value); + return array.reduce( + (hp, item, index) => this.addToHttpParamsDeepObject(hp, `${key}[${index}]`, item), + httpParams, + ); + } else if (typeof value === 'object') { + return Object.keys(value).reduce( + (hp, k) => this.addToHttpParamsDeepObject(hp, `${key}[${k}]`, value[k]), + httpParams, + ); + } + return httpParams.append(key, value.toString()); + } } diff --git a/samples/client/others/typescript-angular-v20/projects/app/src/api.query_param_deep_object.spec.ts b/samples/client/others/typescript-angular-v20/projects/app/src/api.query_param_deep_object.spec.ts index c6b5e11a5316..d05098a27971 100644 --- a/samples/client/others/typescript-angular-v20/projects/app/src/api.query_param_deep_object.spec.ts +++ b/samples/client/others/typescript-angular-v20/projects/app/src/api.query_param_deep_object.spec.ts @@ -3,7 +3,10 @@ import {TestBed} from '@angular/core/testing' import {provideHttpClient} from '@angular/common/http' import {HttpTestingController, provideHttpClientTesting} from '@angular/common/http/testing' import {DefaultService} from '@swagger/typescript-angular-query-param-deep-object' -import {provideApi} from '@swagger/typescript-angular-query-param-form'; +import {provideApi} from '@swagger/typescript-angular-query-param-deep-object'; + +import {BaseService} from '../../../builds/query-param-deep-object/api.base.service'; +import {OpenApiHttpParams, QueryParamStyle} from '../../../builds/query-param-deep-object/query.params'; describe('DeepObject Query Param testing', () => { let httpTesting: HttpTestingController; @@ -35,3 +38,40 @@ describe('DeepObject Query Param testing', () => { expect(req.request.method).toEqual('GET'); }); }); + +// Exercise the generated serializer with shapes not restricted by the sample schema. + +class QuerySerializer extends BaseService { + serialize(value: unknown): string { + return this.addToHttpParams(new OpenApiHttpParams(), 'filter', value, QueryParamStyle.DeepObject, true).toHttpParams().toString(); + } +} + +describe('Nested deepObject serialization', () => { + const serializer = new QuerySerializer(); + + it('preserves nested property names and encodes keys and values', () => { + expect(serializer.serialize({name: {contains: 'A & B'}, 'a b': {value: '+'}})) + .toBe('filter%5Bname%5D%5Bcontains%5D=A%20%26%20B&filter%5Ba%20b%5D%5Bvalue%5D=%2B'); + }); + + it('indexes arrays of objects, primitive arrays and sets', () => { + expect(serializer.serialize({sort: [{field: 'name'}, {field: 'age'}], ids: [1, 2], tags: new Set(['a', 'b'])})) + .toBe('filter%5Bsort%5D%5B0%5D%5Bfield%5D=name&filter%5Bsort%5D%5B1%5D%5Bfield%5D=age&filter%5Bids%5D%5B0%5D=1&filter%5Bids%5D%5B1%5D=2&filter%5Btags%5D%5B0%5D=a&filter%5Btags%5D%5B1%5D=b'); + }); + + it('omits nullish values and empty containers, preserving false, zero and empty strings', () => { + expect(serializer.serialize({missing: null, absent: undefined, nested: {zero: 0, no: false, empty: ''}, array: [], object: {}})) + .toBe('filter%5Bnested%5D%5Bzero%5D=0&filter%5Bnested%5D%5Bno%5D=false&filter%5Bnested%5D%5Bempty%5D='); + expect(serializer.serialize(null)).toBe(''); + }); + + it('serializes dates as ISO strings at any depth', () => { + expect(serializer.serialize({range: {start: new Date('2026-01-02T03:04:05Z')}})) + .toBe('filter%5Brange%5D%5Bstart%5D=2026-01-02T03%3A04%3A05.000Z'); + }); + + it('still rejects a primitive deepObject parameter', () => { + expect(() => serializer.serialize('invalid')).toThrowError('An object must be provided for key filter as it is a deep object'); + }); +}); diff --git a/samples/client/others/typescript-angular/builds/composed-schemas-tagged-unions/README.md b/samples/client/others/typescript-angular/builds/composed-schemas-tagged-unions/README.md index c2ba935ae173..801745dce1da 100644 --- a/samples/client/others/typescript-angular/builds/composed-schemas-tagged-unions/README.md +++ b/samples/client/others/typescript-angular/builds/composed-schemas-tagged-unions/README.md @@ -183,3 +183,13 @@ new Configuration({ [parameter-locations-url]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#parameter-locations [style-values-url]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#style-values [@honoluluhenk/http-param-expander]: https://www.npmjs.com/package/@honoluluhenk/http-param-expander + +## Deep-object query parameters + +For `style: deepObject`, nested objects are serialized using bracket notation, for example +`filter[name][contains]=Alice`. Arrays and sets use zero-based indices, for example +`filter[sort][0][field]=name`. Dates are serialized as ISO strings; null and undefined +values and empty containers are omitted. + +OpenAPI only specifies deepObject serialization for flat objects. This recursive +encoding is a generator extension and requires a server that accepts bracket notation. diff --git a/samples/client/others/typescript-angular/builds/composed-schemas-tagged-unions/api.base.service.ts b/samples/client/others/typescript-angular/builds/composed-schemas-tagged-unions/api.base.service.ts index da9512f79726..a3f5732154dc 100644 --- a/samples/client/others/typescript-angular/builds/composed-schemas-tagged-unions/api.base.service.ts +++ b/samples/client/others/typescript-angular/builds/composed-schemas-tagged-unions/api.base.service.ts @@ -49,7 +49,7 @@ export class BaseService { } return Object.keys(value as Record).reduce( - (hp, k) => hp.append(`${key}[${k}]`, value[k]), + (hp, k) => this.addToHttpParamsDeepObject(hp, `${key}[${k}]`, value[k]), httpParams, ); } else if (paramStyle === QueryParamStyle.Json) { @@ -93,4 +93,28 @@ export class BaseService { } } } + + /** + * Nested deepObject values are an extension to OpenAPI's flat-object serialization. + * Object properties use bracket notation; array and set elements use numeric indices. + */ + private addToHttpParamsDeepObject(httpParams: OpenApiHttpParams, key: string, value: any): OpenApiHttpParams { + if (value === null || value === undefined) { + return httpParams; + } else if (value instanceof Date) { + return httpParams.append(key, value.toISOString()); + } else if (Array.isArray(value) || value instanceof Set) { + const array = Array.isArray(value) ? value : Array.from(value); + return array.reduce( + (hp, item, index) => this.addToHttpParamsDeepObject(hp, `${key}[${index}]`, item), + httpParams, + ); + } else if (typeof value === 'object') { + return Object.keys(value).reduce( + (hp, k) => this.addToHttpParamsDeepObject(hp, `${key}[${k}]`, value[k]), + httpParams, + ); + } + return httpParams.append(key, value.toString()); + } } diff --git a/samples/client/others/typescript-angular/builds/composed-schemas/README.md b/samples/client/others/typescript-angular/builds/composed-schemas/README.md index c2ba935ae173..801745dce1da 100644 --- a/samples/client/others/typescript-angular/builds/composed-schemas/README.md +++ b/samples/client/others/typescript-angular/builds/composed-schemas/README.md @@ -183,3 +183,13 @@ new Configuration({ [parameter-locations-url]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#parameter-locations [style-values-url]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#style-values [@honoluluhenk/http-param-expander]: https://www.npmjs.com/package/@honoluluhenk/http-param-expander + +## Deep-object query parameters + +For `style: deepObject`, nested objects are serialized using bracket notation, for example +`filter[name][contains]=Alice`. Arrays and sets use zero-based indices, for example +`filter[sort][0][field]=name`. Dates are serialized as ISO strings; null and undefined +values and empty containers are omitted. + +OpenAPI only specifies deepObject serialization for flat objects. This recursive +encoding is a generator extension and requires a server that accepts bracket notation. diff --git a/samples/client/others/typescript-angular/builds/composed-schemas/api.base.service.ts b/samples/client/others/typescript-angular/builds/composed-schemas/api.base.service.ts index da9512f79726..a3f5732154dc 100644 --- a/samples/client/others/typescript-angular/builds/composed-schemas/api.base.service.ts +++ b/samples/client/others/typescript-angular/builds/composed-schemas/api.base.service.ts @@ -49,7 +49,7 @@ export class BaseService { } return Object.keys(value as Record).reduce( - (hp, k) => hp.append(`${key}[${k}]`, value[k]), + (hp, k) => this.addToHttpParamsDeepObject(hp, `${key}[${k}]`, value[k]), httpParams, ); } else if (paramStyle === QueryParamStyle.Json) { @@ -93,4 +93,28 @@ export class BaseService { } } } + + /** + * Nested deepObject values are an extension to OpenAPI's flat-object serialization. + * Object properties use bracket notation; array and set elements use numeric indices. + */ + private addToHttpParamsDeepObject(httpParams: OpenApiHttpParams, key: string, value: any): OpenApiHttpParams { + if (value === null || value === undefined) { + return httpParams; + } else if (value instanceof Date) { + return httpParams.append(key, value.toISOString()); + } else if (Array.isArray(value) || value instanceof Set) { + const array = Array.isArray(value) ? value : Array.from(value); + return array.reduce( + (hp, item, index) => this.addToHttpParamsDeepObject(hp, `${key}[${index}]`, item), + httpParams, + ); + } else if (typeof value === 'object') { + return Object.keys(value).reduce( + (hp, k) => this.addToHttpParamsDeepObject(hp, `${key}[${k}]`, value[k]), + httpParams, + ); + } + return httpParams.append(key, value.toString()); + } } diff --git a/samples/client/petstore/typescript-angular-v18-provided-in-root/builds/default/README.md b/samples/client/petstore/typescript-angular-v18-provided-in-root/builds/default/README.md index dbc94fd09277..315d52cfc902 100644 --- a/samples/client/petstore/typescript-angular-v18-provided-in-root/builds/default/README.md +++ b/samples/client/petstore/typescript-angular-v18-provided-in-root/builds/default/README.md @@ -183,3 +183,13 @@ new Configuration({ [parameter-locations-url]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#parameter-locations [style-values-url]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#style-values [@honoluluhenk/http-param-expander]: https://www.npmjs.com/package/@honoluluhenk/http-param-expander + +## Deep-object query parameters + +For `style: deepObject`, nested objects are serialized using bracket notation, for example +`filter[name][contains]=Alice`. Arrays and sets use zero-based indices, for example +`filter[sort][0][field]=name`. Dates are serialized as ISO strings; null and undefined +values and empty containers are omitted. + +OpenAPI only specifies deepObject serialization for flat objects. This recursive +encoding is a generator extension and requires a server that accepts bracket notation. diff --git a/samples/client/petstore/typescript-angular-v18-provided-in-root/builds/default/api.base.service.ts b/samples/client/petstore/typescript-angular-v18-provided-in-root/builds/default/api.base.service.ts index 8a8b8bc6b6f5..be3fe1db2090 100644 --- a/samples/client/petstore/typescript-angular-v18-provided-in-root/builds/default/api.base.service.ts +++ b/samples/client/petstore/typescript-angular-v18-provided-in-root/builds/default/api.base.service.ts @@ -49,7 +49,7 @@ export class BaseService { } return Object.keys(value as Record).reduce( - (hp, k) => hp.append(`${key}[${k}]`, value[k]), + (hp, k) => this.addToHttpParamsDeepObject(hp, `${key}[${k}]`, value[k]), httpParams, ); } else if (paramStyle === QueryParamStyle.Json) { @@ -93,4 +93,28 @@ export class BaseService { } } } + + /** + * Nested deepObject values are an extension to OpenAPI's flat-object serialization. + * Object properties use bracket notation; array and set elements use numeric indices. + */ + private addToHttpParamsDeepObject(httpParams: OpenApiHttpParams, key: string, value: any): OpenApiHttpParams { + if (value === null || value === undefined) { + return httpParams; + } else if (value instanceof Date) { + return httpParams.append(key, value.toISOString()); + } else if (Array.isArray(value) || value instanceof Set) { + const array = Array.isArray(value) ? value : Array.from(value); + return array.reduce( + (hp, item, index) => this.addToHttpParamsDeepObject(hp, `${key}[${index}]`, item), + httpParams, + ); + } else if (typeof value === 'object') { + return Object.keys(value).reduce( + (hp, k) => this.addToHttpParamsDeepObject(hp, `${key}[${k}]`, value[k]), + httpParams, + ); + } + return httpParams.append(key, value.toString()); + } } diff --git a/samples/client/petstore/typescript-angular-v19-provided-in-root/builds/default/README.md b/samples/client/petstore/typescript-angular-v19-provided-in-root/builds/default/README.md index dbc94fd09277..315d52cfc902 100644 --- a/samples/client/petstore/typescript-angular-v19-provided-in-root/builds/default/README.md +++ b/samples/client/petstore/typescript-angular-v19-provided-in-root/builds/default/README.md @@ -183,3 +183,13 @@ new Configuration({ [parameter-locations-url]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#parameter-locations [style-values-url]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#style-values [@honoluluhenk/http-param-expander]: https://www.npmjs.com/package/@honoluluhenk/http-param-expander + +## Deep-object query parameters + +For `style: deepObject`, nested objects are serialized using bracket notation, for example +`filter[name][contains]=Alice`. Arrays and sets use zero-based indices, for example +`filter[sort][0][field]=name`. Dates are serialized as ISO strings; null and undefined +values and empty containers are omitted. + +OpenAPI only specifies deepObject serialization for flat objects. This recursive +encoding is a generator extension and requires a server that accepts bracket notation. diff --git a/samples/client/petstore/typescript-angular-v19-provided-in-root/builds/default/api.base.service.ts b/samples/client/petstore/typescript-angular-v19-provided-in-root/builds/default/api.base.service.ts index 8a8b8bc6b6f5..be3fe1db2090 100644 --- a/samples/client/petstore/typescript-angular-v19-provided-in-root/builds/default/api.base.service.ts +++ b/samples/client/petstore/typescript-angular-v19-provided-in-root/builds/default/api.base.service.ts @@ -49,7 +49,7 @@ export class BaseService { } return Object.keys(value as Record).reduce( - (hp, k) => hp.append(`${key}[${k}]`, value[k]), + (hp, k) => this.addToHttpParamsDeepObject(hp, `${key}[${k}]`, value[k]), httpParams, ); } else if (paramStyle === QueryParamStyle.Json) { @@ -93,4 +93,28 @@ export class BaseService { } } } + + /** + * Nested deepObject values are an extension to OpenAPI's flat-object serialization. + * Object properties use bracket notation; array and set elements use numeric indices. + */ + private addToHttpParamsDeepObject(httpParams: OpenApiHttpParams, key: string, value: any): OpenApiHttpParams { + if (value === null || value === undefined) { + return httpParams; + } else if (value instanceof Date) { + return httpParams.append(key, value.toISOString()); + } else if (Array.isArray(value) || value instanceof Set) { + const array = Array.isArray(value) ? value : Array.from(value); + return array.reduce( + (hp, item, index) => this.addToHttpParamsDeepObject(hp, `${key}[${index}]`, item), + httpParams, + ); + } else if (typeof value === 'object') { + return Object.keys(value).reduce( + (hp, k) => this.addToHttpParamsDeepObject(hp, `${key}[${k}]`, value[k]), + httpParams, + ); + } + return httpParams.append(key, value.toString()); + } } diff --git a/samples/client/petstore/typescript-angular-v19-with-angular-dependency-params/builds/default/README.md b/samples/client/petstore/typescript-angular-v19-with-angular-dependency-params/builds/default/README.md index 92fc8cb8eba8..7c83423d62b0 100644 --- a/samples/client/petstore/typescript-angular-v19-with-angular-dependency-params/builds/default/README.md +++ b/samples/client/petstore/typescript-angular-v19-with-angular-dependency-params/builds/default/README.md @@ -183,3 +183,13 @@ new Configuration({ [parameter-locations-url]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#parameter-locations [style-values-url]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#style-values [@honoluluhenk/http-param-expander]: https://www.npmjs.com/package/@honoluluhenk/http-param-expander + +## Deep-object query parameters + +For `style: deepObject`, nested objects are serialized using bracket notation, for example +`filter[name][contains]=Alice`. Arrays and sets use zero-based indices, for example +`filter[sort][0][field]=name`. Dates are serialized as ISO strings; null and undefined +values and empty containers are omitted. + +OpenAPI only specifies deepObject serialization for flat objects. This recursive +encoding is a generator extension and requires a server that accepts bracket notation. diff --git a/samples/client/petstore/typescript-angular-v19-with-angular-dependency-params/builds/default/api.base.service.ts b/samples/client/petstore/typescript-angular-v19-with-angular-dependency-params/builds/default/api.base.service.ts index 8a8b8bc6b6f5..be3fe1db2090 100644 --- a/samples/client/petstore/typescript-angular-v19-with-angular-dependency-params/builds/default/api.base.service.ts +++ b/samples/client/petstore/typescript-angular-v19-with-angular-dependency-params/builds/default/api.base.service.ts @@ -49,7 +49,7 @@ export class BaseService { } return Object.keys(value as Record).reduce( - (hp, k) => hp.append(`${key}[${k}]`, value[k]), + (hp, k) => this.addToHttpParamsDeepObject(hp, `${key}[${k}]`, value[k]), httpParams, ); } else if (paramStyle === QueryParamStyle.Json) { @@ -93,4 +93,28 @@ export class BaseService { } } } + + /** + * Nested deepObject values are an extension to OpenAPI's flat-object serialization. + * Object properties use bracket notation; array and set elements use numeric indices. + */ + private addToHttpParamsDeepObject(httpParams: OpenApiHttpParams, key: string, value: any): OpenApiHttpParams { + if (value === null || value === undefined) { + return httpParams; + } else if (value instanceof Date) { + return httpParams.append(key, value.toISOString()); + } else if (Array.isArray(value) || value instanceof Set) { + const array = Array.isArray(value) ? value : Array.from(value); + return array.reduce( + (hp, item, index) => this.addToHttpParamsDeepObject(hp, `${key}[${index}]`, item), + httpParams, + ); + } else if (typeof value === 'object') { + return Object.keys(value).reduce( + (hp, k) => this.addToHttpParamsDeepObject(hp, `${key}[${k}]`, value[k]), + httpParams, + ); + } + return httpParams.append(key, value.toString()); + } } diff --git a/samples/client/petstore/typescript-angular-v19/builds/default/README.md b/samples/client/petstore/typescript-angular-v19/builds/default/README.md index 2aedaf8b75cf..d2d3af5af00b 100644 --- a/samples/client/petstore/typescript-angular-v19/builds/default/README.md +++ b/samples/client/petstore/typescript-angular-v19/builds/default/README.md @@ -183,3 +183,13 @@ new Configuration({ [parameter-locations-url]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#parameter-locations [style-values-url]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#style-values [@honoluluhenk/http-param-expander]: https://www.npmjs.com/package/@honoluluhenk/http-param-expander + +## Deep-object query parameters + +For `style: deepObject`, nested objects are serialized using bracket notation, for example +`filter[name][contains]=Alice`. Arrays and sets use zero-based indices, for example +`filter[sort][0][field]=name`. Dates are serialized as ISO strings; null and undefined +values and empty containers are omitted. + +OpenAPI only specifies deepObject serialization for flat objects. This recursive +encoding is a generator extension and requires a server that accepts bracket notation. diff --git a/samples/client/petstore/typescript-angular-v19/builds/default/api.base.service.ts b/samples/client/petstore/typescript-angular-v19/builds/default/api.base.service.ts index 8a8b8bc6b6f5..be3fe1db2090 100644 --- a/samples/client/petstore/typescript-angular-v19/builds/default/api.base.service.ts +++ b/samples/client/petstore/typescript-angular-v19/builds/default/api.base.service.ts @@ -49,7 +49,7 @@ export class BaseService { } return Object.keys(value as Record).reduce( - (hp, k) => hp.append(`${key}[${k}]`, value[k]), + (hp, k) => this.addToHttpParamsDeepObject(hp, `${key}[${k}]`, value[k]), httpParams, ); } else if (paramStyle === QueryParamStyle.Json) { @@ -93,4 +93,28 @@ export class BaseService { } } } + + /** + * Nested deepObject values are an extension to OpenAPI's flat-object serialization. + * Object properties use bracket notation; array and set elements use numeric indices. + */ + private addToHttpParamsDeepObject(httpParams: OpenApiHttpParams, key: string, value: any): OpenApiHttpParams { + if (value === null || value === undefined) { + return httpParams; + } else if (value instanceof Date) { + return httpParams.append(key, value.toISOString()); + } else if (Array.isArray(value) || value instanceof Set) { + const array = Array.isArray(value) ? value : Array.from(value); + return array.reduce( + (hp, item, index) => this.addToHttpParamsDeepObject(hp, `${key}[${index}]`, item), + httpParams, + ); + } else if (typeof value === 'object') { + return Object.keys(value).reduce( + (hp, k) => this.addToHttpParamsDeepObject(hp, `${key}[${k}]`, value[k]), + httpParams, + ); + } + return httpParams.append(key, value.toString()); + } } diff --git a/samples/client/petstore/typescript-angular-v20-provided-in-root/builds/default/README.md b/samples/client/petstore/typescript-angular-v20-provided-in-root/builds/default/README.md index dbc94fd09277..315d52cfc902 100644 --- a/samples/client/petstore/typescript-angular-v20-provided-in-root/builds/default/README.md +++ b/samples/client/petstore/typescript-angular-v20-provided-in-root/builds/default/README.md @@ -183,3 +183,13 @@ new Configuration({ [parameter-locations-url]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#parameter-locations [style-values-url]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#style-values [@honoluluhenk/http-param-expander]: https://www.npmjs.com/package/@honoluluhenk/http-param-expander + +## Deep-object query parameters + +For `style: deepObject`, nested objects are serialized using bracket notation, for example +`filter[name][contains]=Alice`. Arrays and sets use zero-based indices, for example +`filter[sort][0][field]=name`. Dates are serialized as ISO strings; null and undefined +values and empty containers are omitted. + +OpenAPI only specifies deepObject serialization for flat objects. This recursive +encoding is a generator extension and requires a server that accepts bracket notation. diff --git a/samples/client/petstore/typescript-angular-v20-provided-in-root/builds/default/api.base.service.ts b/samples/client/petstore/typescript-angular-v20-provided-in-root/builds/default/api.base.service.ts index 8a8b8bc6b6f5..be3fe1db2090 100644 --- a/samples/client/petstore/typescript-angular-v20-provided-in-root/builds/default/api.base.service.ts +++ b/samples/client/petstore/typescript-angular-v20-provided-in-root/builds/default/api.base.service.ts @@ -49,7 +49,7 @@ export class BaseService { } return Object.keys(value as Record).reduce( - (hp, k) => hp.append(`${key}[${k}]`, value[k]), + (hp, k) => this.addToHttpParamsDeepObject(hp, `${key}[${k}]`, value[k]), httpParams, ); } else if (paramStyle === QueryParamStyle.Json) { @@ -93,4 +93,28 @@ export class BaseService { } } } + + /** + * Nested deepObject values are an extension to OpenAPI's flat-object serialization. + * Object properties use bracket notation; array and set elements use numeric indices. + */ + private addToHttpParamsDeepObject(httpParams: OpenApiHttpParams, key: string, value: any): OpenApiHttpParams { + if (value === null || value === undefined) { + return httpParams; + } else if (value instanceof Date) { + return httpParams.append(key, value.toISOString()); + } else if (Array.isArray(value) || value instanceof Set) { + const array = Array.isArray(value) ? value : Array.from(value); + return array.reduce( + (hp, item, index) => this.addToHttpParamsDeepObject(hp, `${key}[${index}]`, item), + httpParams, + ); + } else if (typeof value === 'object') { + return Object.keys(value).reduce( + (hp, k) => this.addToHttpParamsDeepObject(hp, `${key}[${k}]`, value[k]), + httpParams, + ); + } + return httpParams.append(key, value.toString()); + } } diff --git a/samples/client/petstore/typescript-angular-v20/builds/default/README.md b/samples/client/petstore/typescript-angular-v20/builds/default/README.md index 6b359c947203..0433b3d341b4 100644 --- a/samples/client/petstore/typescript-angular-v20/builds/default/README.md +++ b/samples/client/petstore/typescript-angular-v20/builds/default/README.md @@ -183,3 +183,13 @@ new Configuration({ [parameter-locations-url]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#parameter-locations [style-values-url]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#style-values [@honoluluhenk/http-param-expander]: https://www.npmjs.com/package/@honoluluhenk/http-param-expander + +## Deep-object query parameters + +For `style: deepObject`, nested objects are serialized using bracket notation, for example +`filter[name][contains]=Alice`. Arrays and sets use zero-based indices, for example +`filter[sort][0][field]=name`. Dates are serialized as ISO strings; null and undefined +values and empty containers are omitted. + +OpenAPI only specifies deepObject serialization for flat objects. This recursive +encoding is a generator extension and requires a server that accepts bracket notation. diff --git a/samples/client/petstore/typescript-angular-v20/builds/default/api.base.service.ts b/samples/client/petstore/typescript-angular-v20/builds/default/api.base.service.ts index 8a8b8bc6b6f5..be3fe1db2090 100644 --- a/samples/client/petstore/typescript-angular-v20/builds/default/api.base.service.ts +++ b/samples/client/petstore/typescript-angular-v20/builds/default/api.base.service.ts @@ -49,7 +49,7 @@ export class BaseService { } return Object.keys(value as Record).reduce( - (hp, k) => hp.append(`${key}[${k}]`, value[k]), + (hp, k) => this.addToHttpParamsDeepObject(hp, `${key}[${k}]`, value[k]), httpParams, ); } else if (paramStyle === QueryParamStyle.Json) { @@ -93,4 +93,28 @@ export class BaseService { } } } + + /** + * Nested deepObject values are an extension to OpenAPI's flat-object serialization. + * Object properties use bracket notation; array and set elements use numeric indices. + */ + private addToHttpParamsDeepObject(httpParams: OpenApiHttpParams, key: string, value: any): OpenApiHttpParams { + if (value === null || value === undefined) { + return httpParams; + } else if (value instanceof Date) { + return httpParams.append(key, value.toISOString()); + } else if (Array.isArray(value) || value instanceof Set) { + const array = Array.isArray(value) ? value : Array.from(value); + return array.reduce( + (hp, item, index) => this.addToHttpParamsDeepObject(hp, `${key}[${index}]`, item), + httpParams, + ); + } else if (typeof value === 'object') { + return Object.keys(value).reduce( + (hp, k) => this.addToHttpParamsDeepObject(hp, `${key}[${k}]`, value[k]), + httpParams, + ); + } + return httpParams.append(key, value.toString()); + } } diff --git a/samples/client/petstore/typescript-angular-v21-provided-in-root/builds/default/README.md b/samples/client/petstore/typescript-angular-v21-provided-in-root/builds/default/README.md index dbc94fd09277..315d52cfc902 100644 --- a/samples/client/petstore/typescript-angular-v21-provided-in-root/builds/default/README.md +++ b/samples/client/petstore/typescript-angular-v21-provided-in-root/builds/default/README.md @@ -183,3 +183,13 @@ new Configuration({ [parameter-locations-url]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#parameter-locations [style-values-url]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#style-values [@honoluluhenk/http-param-expander]: https://www.npmjs.com/package/@honoluluhenk/http-param-expander + +## Deep-object query parameters + +For `style: deepObject`, nested objects are serialized using bracket notation, for example +`filter[name][contains]=Alice`. Arrays and sets use zero-based indices, for example +`filter[sort][0][field]=name`. Dates are serialized as ISO strings; null and undefined +values and empty containers are omitted. + +OpenAPI only specifies deepObject serialization for flat objects. This recursive +encoding is a generator extension and requires a server that accepts bracket notation. diff --git a/samples/client/petstore/typescript-angular-v21-provided-in-root/builds/default/api.base.service.ts b/samples/client/petstore/typescript-angular-v21-provided-in-root/builds/default/api.base.service.ts index 8a8b8bc6b6f5..be3fe1db2090 100644 --- a/samples/client/petstore/typescript-angular-v21-provided-in-root/builds/default/api.base.service.ts +++ b/samples/client/petstore/typescript-angular-v21-provided-in-root/builds/default/api.base.service.ts @@ -49,7 +49,7 @@ export class BaseService { } return Object.keys(value as Record).reduce( - (hp, k) => hp.append(`${key}[${k}]`, value[k]), + (hp, k) => this.addToHttpParamsDeepObject(hp, `${key}[${k}]`, value[k]), httpParams, ); } else if (paramStyle === QueryParamStyle.Json) { @@ -93,4 +93,28 @@ export class BaseService { } } } + + /** + * Nested deepObject values are an extension to OpenAPI's flat-object serialization. + * Object properties use bracket notation; array and set elements use numeric indices. + */ + private addToHttpParamsDeepObject(httpParams: OpenApiHttpParams, key: string, value: any): OpenApiHttpParams { + if (value === null || value === undefined) { + return httpParams; + } else if (value instanceof Date) { + return httpParams.append(key, value.toISOString()); + } else if (Array.isArray(value) || value instanceof Set) { + const array = Array.isArray(value) ? value : Array.from(value); + return array.reduce( + (hp, item, index) => this.addToHttpParamsDeepObject(hp, `${key}[${index}]`, item), + httpParams, + ); + } else if (typeof value === 'object') { + return Object.keys(value).reduce( + (hp, k) => this.addToHttpParamsDeepObject(hp, `${key}[${k}]`, value[k]), + httpParams, + ); + } + return httpParams.append(key, value.toString()); + } } diff --git a/samples/client/petstore/typescript-angular-v21/builds/default/README.md b/samples/client/petstore/typescript-angular-v21/builds/default/README.md index fe62ca9c4cb5..421d6c0bda2e 100644 --- a/samples/client/petstore/typescript-angular-v21/builds/default/README.md +++ b/samples/client/petstore/typescript-angular-v21/builds/default/README.md @@ -183,3 +183,13 @@ new Configuration({ [parameter-locations-url]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#parameter-locations [style-values-url]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#style-values [@honoluluhenk/http-param-expander]: https://www.npmjs.com/package/@honoluluhenk/http-param-expander + +## Deep-object query parameters + +For `style: deepObject`, nested objects are serialized using bracket notation, for example +`filter[name][contains]=Alice`. Arrays and sets use zero-based indices, for example +`filter[sort][0][field]=name`. Dates are serialized as ISO strings; null and undefined +values and empty containers are omitted. + +OpenAPI only specifies deepObject serialization for flat objects. This recursive +encoding is a generator extension and requires a server that accepts bracket notation. diff --git a/samples/client/petstore/typescript-angular-v21/builds/default/api.base.service.ts b/samples/client/petstore/typescript-angular-v21/builds/default/api.base.service.ts index 8a8b8bc6b6f5..be3fe1db2090 100644 --- a/samples/client/petstore/typescript-angular-v21/builds/default/api.base.service.ts +++ b/samples/client/petstore/typescript-angular-v21/builds/default/api.base.service.ts @@ -49,7 +49,7 @@ export class BaseService { } return Object.keys(value as Record).reduce( - (hp, k) => hp.append(`${key}[${k}]`, value[k]), + (hp, k) => this.addToHttpParamsDeepObject(hp, `${key}[${k}]`, value[k]), httpParams, ); } else if (paramStyle === QueryParamStyle.Json) { @@ -93,4 +93,28 @@ export class BaseService { } } } + + /** + * Nested deepObject values are an extension to OpenAPI's flat-object serialization. + * Object properties use bracket notation; array and set elements use numeric indices. + */ + private addToHttpParamsDeepObject(httpParams: OpenApiHttpParams, key: string, value: any): OpenApiHttpParams { + if (value === null || value === undefined) { + return httpParams; + } else if (value instanceof Date) { + return httpParams.append(key, value.toISOString()); + } else if (Array.isArray(value) || value instanceof Set) { + const array = Array.isArray(value) ? value : Array.from(value); + return array.reduce( + (hp, item, index) => this.addToHttpParamsDeepObject(hp, `${key}[${index}]`, item), + httpParams, + ); + } else if (typeof value === 'object') { + return Object.keys(value).reduce( + (hp, k) => this.addToHttpParamsDeepObject(hp, `${key}[${k}]`, value[k]), + httpParams, + ); + } + return httpParams.append(key, value.toString()); + } } diff --git a/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/default/README.md b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/default/README.md index dbc94fd09277..315d52cfc902 100644 --- a/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/default/README.md +++ b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/default/README.md @@ -183,3 +183,13 @@ new Configuration({ [parameter-locations-url]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#parameter-locations [style-values-url]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#style-values [@honoluluhenk/http-param-expander]: https://www.npmjs.com/package/@honoluluhenk/http-param-expander + +## Deep-object query parameters + +For `style: deepObject`, nested objects are serialized using bracket notation, for example +`filter[name][contains]=Alice`. Arrays and sets use zero-based indices, for example +`filter[sort][0][field]=name`. Dates are serialized as ISO strings; null and undefined +values and empty containers are omitted. + +OpenAPI only specifies deepObject serialization for flat objects. This recursive +encoding is a generator extension and requires a server that accepts bracket notation. diff --git a/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/default/api.base.service.ts b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/default/api.base.service.ts index 8a8b8bc6b6f5..be3fe1db2090 100644 --- a/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/default/api.base.service.ts +++ b/samples/client/petstore/typescript-angular-v22-provided-in-root/builds/default/api.base.service.ts @@ -49,7 +49,7 @@ export class BaseService { } return Object.keys(value as Record).reduce( - (hp, k) => hp.append(`${key}[${k}]`, value[k]), + (hp, k) => this.addToHttpParamsDeepObject(hp, `${key}[${k}]`, value[k]), httpParams, ); } else if (paramStyle === QueryParamStyle.Json) { @@ -93,4 +93,28 @@ export class BaseService { } } } + + /** + * Nested deepObject values are an extension to OpenAPI's flat-object serialization. + * Object properties use bracket notation; array and set elements use numeric indices. + */ + private addToHttpParamsDeepObject(httpParams: OpenApiHttpParams, key: string, value: any): OpenApiHttpParams { + if (value === null || value === undefined) { + return httpParams; + } else if (value instanceof Date) { + return httpParams.append(key, value.toISOString()); + } else if (Array.isArray(value) || value instanceof Set) { + const array = Array.isArray(value) ? value : Array.from(value); + return array.reduce( + (hp, item, index) => this.addToHttpParamsDeepObject(hp, `${key}[${index}]`, item), + httpParams, + ); + } else if (typeof value === 'object') { + return Object.keys(value).reduce( + (hp, k) => this.addToHttpParamsDeepObject(hp, `${key}[${k}]`, value[k]), + httpParams, + ); + } + return httpParams.append(key, value.toString()); + } } diff --git a/samples/client/petstore/typescript-angular-v22/builds/default/README.md b/samples/client/petstore/typescript-angular-v22/builds/default/README.md index 9913c559ae5f..d40b97959d19 100644 --- a/samples/client/petstore/typescript-angular-v22/builds/default/README.md +++ b/samples/client/petstore/typescript-angular-v22/builds/default/README.md @@ -183,3 +183,13 @@ new Configuration({ [parameter-locations-url]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#parameter-locations [style-values-url]: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#style-values [@honoluluhenk/http-param-expander]: https://www.npmjs.com/package/@honoluluhenk/http-param-expander + +## Deep-object query parameters + +For `style: deepObject`, nested objects are serialized using bracket notation, for example +`filter[name][contains]=Alice`. Arrays and sets use zero-based indices, for example +`filter[sort][0][field]=name`. Dates are serialized as ISO strings; null and undefined +values and empty containers are omitted. + +OpenAPI only specifies deepObject serialization for flat objects. This recursive +encoding is a generator extension and requires a server that accepts bracket notation. diff --git a/samples/client/petstore/typescript-angular-v22/builds/default/api.base.service.ts b/samples/client/petstore/typescript-angular-v22/builds/default/api.base.service.ts index 8a8b8bc6b6f5..be3fe1db2090 100644 --- a/samples/client/petstore/typescript-angular-v22/builds/default/api.base.service.ts +++ b/samples/client/petstore/typescript-angular-v22/builds/default/api.base.service.ts @@ -49,7 +49,7 @@ export class BaseService { } return Object.keys(value as Record).reduce( - (hp, k) => hp.append(`${key}[${k}]`, value[k]), + (hp, k) => this.addToHttpParamsDeepObject(hp, `${key}[${k}]`, value[k]), httpParams, ); } else if (paramStyle === QueryParamStyle.Json) { @@ -93,4 +93,28 @@ export class BaseService { } } } + + /** + * Nested deepObject values are an extension to OpenAPI's flat-object serialization. + * Object properties use bracket notation; array and set elements use numeric indices. + */ + private addToHttpParamsDeepObject(httpParams: OpenApiHttpParams, key: string, value: any): OpenApiHttpParams { + if (value === null || value === undefined) { + return httpParams; + } else if (value instanceof Date) { + return httpParams.append(key, value.toISOString()); + } else if (Array.isArray(value) || value instanceof Set) { + const array = Array.isArray(value) ? value : Array.from(value); + return array.reduce( + (hp, item, index) => this.addToHttpParamsDeepObject(hp, `${key}[${index}]`, item), + httpParams, + ); + } else if (typeof value === 'object') { + return Object.keys(value).reduce( + (hp, k) => this.addToHttpParamsDeepObject(hp, `${key}[${k}]`, value[k]), + httpParams, + ); + } + return httpParams.append(key, value.toString()); + } } From 2f1cfcf4410bbf55796112f312e0bc015dc630ad Mon Sep 17 00:00:00 2001 From: Wiebren Braakman Date: Wed, 23 Sep 2026 16:14:36 +0200 Subject: [PATCH 2/3] [typescript-fetch] fix: explode object query parameters (#24803) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: [typescript-fetch] explode object query parameters A query parameter whose schema is an object and whose style/explode are left at their defaults — style: form, explode: true — must go on the wire as one parameter per entry, keyed by the property name alone. The typescript-fetch client bracketed it as filter[category]=books instead. typescript-fetch/apis.mustache gated the explode branch on isContainer, which DefaultCodegen.fromParameter only sets via updateParameterForMap, which in turn needs ModelUtils.isMapSchema (so, additionalProperties). A bare type: object instead goes through setTypeProperties, which sets isMap and isFreeFormObject and leaves isContainer false. The parameter was therefore assigned whole and the runtime's querystringSingleKey bracketed it. The branch is now keyed on isMap; inside {{^isArray}}, isContainer implies isMap, so this is the same condition plus free-form objects. TypeScriptFetchClientCodegen's ExtendedCodegenParameter copy constructor copied isExplode and style but not isDeepObject, isFormStyle, isMatrix, isAllowEmptyValue, isSpaceDelimited or isPipeDelimited, so all six were false in every typescript-fetch template regardless of the document. This is independent of the template fix, and it is why fixing the template alone would have broken deepObject parameters — they are explode: true too. The loop body gains an `as any` cast on the indexing. It is not needed for a declared map, but a free-form object is typed `object` and object[key] is error TS7053 under strict, which is how most consumers compile. * fix: [typescript-fetch] leave out a null entry of an exploded object The runtime turned a null or undefined entry into k=null or k=undefined; the loop now skips it, as the other ports do. Only isDeepObject is read by the templates, so the copy constructor keeps that flag alone. The mustache comment shrinks to one line, and the shared fixture's description names the three combinations it covers. Co-Authored-By: Claude Opus 5.5 (1M context) * fix: [typescript-fetch] keep an exploded entry named __proto__ Assigning queryParameters['__proto__'] hits the Object.prototype setter and drops the entry, while the runtime keeps such a key under deepObject and in nested objects. The loop now defines the property instead. Co-Authored-By: Claude Opus 5.5 (1M context) * test: [typescript-fetch] say the null-skip assertion pins emitted code The codegen test cannot run the generated client, so its comment now says it locks the code shape; the wire behaviour was checked separately. Co-Authored-By: Claude Opus 5.5 (1M context) * Apply suggestion from @cubic-dev-ai[bot] Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) Co-authored-by: Esteban Gehring Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- .../TypeScriptFetchClientCodegen.java | 8 +++++ .../resources/typescript-fetch/apis.mustache | 19 ++++++++---- .../TypeScriptFetchClientCodegenTest.java | 30 +++++++++++++++++++ .../builds/default-v3.0/apis/FakeApi.ts | 5 +++- .../builds/kebab-case/apis/fake-api.ts | 5 +++- .../snakecase-discriminator/apis/FakeApi.ts | 5 +++- 6 files changed, 64 insertions(+), 8 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptFetchClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptFetchClientCodegen.java index 59bd7117a7f2..411521087608 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptFetchClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptFetchClientCodegen.java @@ -1574,6 +1574,14 @@ public ExtendedCodegenParameter(CodegenParameter cp) { this.isPrimitiveType = cp.isPrimitiveType; this.isModel = cp.isModel; this.isExplode = cp.isExplode; + this.isExplode = cp.isExplode; + this.isDeepObject = cp.isDeepObject; + this.isFormStyle = cp.isFormStyle; + this.isMatrix = cp.isMatrix; + this.isAllowEmptyValue = cp.isAllowEmptyValue; + this.isSpaceDelimited = cp.isSpaceDelimited; + this.isPipeDelimited = cp.isPipeDelimited; + this.baseName = cp.baseName; this.paramName = cp.paramName; this.dataType = cp.dataType; diff --git a/modules/openapi-generator/src/main/resources/typescript-fetch/apis.mustache b/modules/openapi-generator/src/main/resources/typescript-fetch/apis.mustache index 7fa306995134..0980e870e58a 100644 --- a/modules/openapi-generator/src/main/resources/typescript-fetch/apis.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-fetch/apis.mustache @@ -202,14 +202,23 @@ export class {{classname}} extends runtime.BaseAPI { {{^isArray}} if (requestParameters['{{paramName}}'] != null) { {{#isExplode}} - {{#isContainer}} + {{! form + explode: one parameter per entry. isMap (not isContainer) also covers free-form objects; deepObject is assigned whole so the runtime brackets it. }} + {{#isDeepObject}} +{{>apisAssignQueryParam}} + {{/isDeepObject}} + {{^isDeepObject}} + {{#isMap}} for (let key of Object.keys(requestParameters['{{paramName}}'])) { - queryParameters[key] = requestParameters['{{paramName}}'][key]; + const value = (requestParameters['{{paramName}}'] as any)[key]; + if (value != null) { + Object.defineProperty(queryParameters, key, { value, enumerable: true, writable: true, configurable: true }); + } } - {{/isContainer}} - {{^isContainer}} + {{/isMap}} + {{^isMap}} {{>apisAssignQueryParam}} - {{/isContainer}} + {{/isMap}} + {{/isDeepObject}} {{/isExplode}} {{^isExplode}} {{>apisAssignQueryParam}} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/fetch/TypeScriptFetchClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/fetch/TypeScriptFetchClientCodegenTest.java index 443e534b03a9..521196b16af0 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/fetch/TypeScriptFetchClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/fetch/TypeScriptFetchClientCodegenTest.java @@ -1234,6 +1234,36 @@ public void testDateFormatIsSerializedAsACalendarDate() throws IOException { TestUtils.assertFileContains(api, "formParams.append('createdAt', runtime.serializeDateTime(requestParameters['createdAt'] as any))"); } + @Test(description = "Verify a form style, exploded map query parameter goes on the wire one entry per parameter") + public void testExplodedObjectQueryParameter() throws IOException { + File output = generate(new HashMap<>(), "src/test/resources/3_0/exploded-object-query-param.yaml"); + Path api = Paths.get(output + "/apis/DefaultApi.ts"); + + // form style with explode - the default - puts every entry on the wire under its own + // property name. A free-form object is isMap but not isContainer, so it used to fall + // through to a whole-object assignment and end up bracketed by the runtime. + TestUtils.assertFileContains(api, + "for (let key of Object.keys(requestParameters['filter'])) {", + "const value = (requestParameters['filter'] as any)[key];"); + TestUtils.assertFileNotContains(api, "queryParameters['filter'] = requestParameters['filter'];"); + + // a declared map behaves the same way + TestUtils.assertFileContains(api, + "for (let key of Object.keys(requestParameters['typedFilter'])) {", + "const value = (requestParameters['typedFilter'] as any)[key];"); + + // pins the emitted loop: a null or undefined entry is skipped, and the entry is defined rather than assigned so a key named __proto__ survives + TestUtils.assertFileContains(api, "if (value != null) { Object.defineProperty(queryParameters, key, { value, enumerable: true, writable: true, configurable: true }); }"); + + // deepObject nests under the parameter name, which the runtime does for a whole object + TestUtils.assertFileContains(api, "queryParameters['deepFilter'] = requestParameters['deepFilter'];"); + TestUtils.assertFileNotContains(api, "Object.keys(requestParameters['deepFilter'])"); + + // without explode the object stays a single parameter + TestUtils.assertFileContains(api, "queryParameters['flatFilter'] = requestParameters['flatFilter'];"); + TestUtils.assertFileNotContains(api, "Object.keys(requestParameters['flatFilter'])"); + } + private static final String DATE_HANDLING_SPEC = "src/test/resources/3_0/typescript-fetch/date-handling.yaml"; private static File generate( diff --git a/samples/client/petstore/typescript-fetch/builds/default-v3.0/apis/FakeApi.ts b/samples/client/petstore/typescript-fetch/builds/default-v3.0/apis/FakeApi.ts index f64fcdc89bf5..824be8eef113 100644 --- a/samples/client/petstore/typescript-fetch/builds/default-v3.0/apis/FakeApi.ts +++ b/samples/client/petstore/typescript-fetch/builds/default-v3.0/apis/FakeApi.ts @@ -1560,7 +1560,10 @@ export class FakeApi extends runtime.BaseAPI { if (requestParameters['language'] != null) { for (let key of Object.keys(requestParameters['language'])) { - queryParameters[key] = requestParameters['language'][key]; + const value = (requestParameters['language'] as any)[key]; + if (value != null) { + Object.defineProperty(queryParameters, key, { value, enumerable: true, writable: true, configurable: true }); + } } } diff --git a/samples/client/petstore/typescript-fetch/builds/kebab-case/apis/fake-api.ts b/samples/client/petstore/typescript-fetch/builds/kebab-case/apis/fake-api.ts index 290f6b63a71d..fda7529f8dfd 100644 --- a/samples/client/petstore/typescript-fetch/builds/kebab-case/apis/fake-api.ts +++ b/samples/client/petstore/typescript-fetch/builds/kebab-case/apis/fake-api.ts @@ -1560,7 +1560,10 @@ export class FakeApi extends runtime.BaseAPI { if (requestParameters['language'] != null) { for (let key of Object.keys(requestParameters['language'])) { - queryParameters[key] = requestParameters['language'][key]; + const value = (requestParameters['language'] as any)[key]; + if (value != null) { + Object.defineProperty(queryParameters, key, { value, enumerable: true, writable: true, configurable: true }); + } } } diff --git a/samples/client/petstore/typescript-fetch/builds/snakecase-discriminator/apis/FakeApi.ts b/samples/client/petstore/typescript-fetch/builds/snakecase-discriminator/apis/FakeApi.ts index 992042979312..57d0e86a9a03 100644 --- a/samples/client/petstore/typescript-fetch/builds/snakecase-discriminator/apis/FakeApi.ts +++ b/samples/client/petstore/typescript-fetch/builds/snakecase-discriminator/apis/FakeApi.ts @@ -1378,7 +1378,10 @@ export class FakeApi extends runtime.BaseAPI { if (requestParameters['language'] != null) { for (let key of Object.keys(requestParameters['language'])) { - queryParameters[key] = requestParameters['language'][key]; + const value = (requestParameters['language'] as any)[key]; + if (value != null) { + Object.defineProperty(queryParameters, key, { value, enumerable: true, writable: true, configurable: true }); + } } } From 6f77724ce5d4a9d61ccb91fde60d80f1c38b0bed Mon Sep 17 00:00:00 2001 From: Wiebren Braakman Date: Wed, 23 Sep 2026 19:06:04 +0200 Subject: [PATCH 3/3] [swift5][swift6] fix: recursive schemas generate structs of infinite size (#24898) * [swift] fix: models on a reference cycle become classes so they compile A struct that stores itself inline - through any chain of model-typed properties, Optional included - has infinite size: "value type cannot have a stored property that recursively contains it", and every struct that embeds it is infinite in turn. The generators emitted structs unconditionally, so any self- or mutually-referencing schema produced a client that does not compile; the only escape was useClasses=true, which turns every model into a class. Detect inline reference cycles in postProcessAllModels (containers break recursion on the heap and are not edges) and render only the models on a cycle as final classes - heap allocation provides the indirection, the wire format is unchanged, and everything else stays a struct. In swift6 a recursion-breaking class is @unchecked Sendable, the way readonlyProperties classes already are, so the Sendable structs embedding it still conform. The per-model rendering flag also carries the global useClasses value, and the useClasses samples (urlsession, vapor, swift5 and swift6 alike) regenerate byte-identical. Fixes #15240 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01GcwZ1arjLZNpetHz2a3TJz * [swift] fix: allOf composition is not an inline-storage edge Review pointed out that allOf parents are flattened into allVars rather than stored inline, so treating the composed reference as an edge could mark models that store nothing recursively. Real cycles introduced by the flattening still surface through the allVars properties themselves; oneOf/anyOf keep their edges, since those render as enums with inline associated values. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01GcwZ1arjLZNpetHz2a3TJz * [swift5][swift6] test: allOf is not a storage edge; tidy the cycle detection Pin bb5244e0's behaviour: Base references Derived, which inherits Base through allOf. Only Derived is on a cycle (through its flattened `related`), so Base must stay a struct; with allOf counted as an edge it became a class. Also shortens the Javadoc, drops a double blank line and inlines swift5's `recursive` local. Co-Authored-By: Claude Opus 5.5 (1M context) --------- Co-authored-by: Claude Fable 5 --- .../languages/Swift5ClientCodegen.java | 57 +++++++++++++++++ .../languages/Swift6ClientCodegen.java | 64 +++++++++++++++++++ .../resources/swift5/modelObject.mustache | 6 +- .../resources/swift6/modelObject.mustache | 6 +- .../swift5/Swift5ClientCodegenTest.java | 25 ++++++++ .../swift6/Swift6ClientCodegenTest.java | 25 ++++++++ .../resources/3_0/swift/recursive-models.yaml | 63 ++++++++++++++++++ 7 files changed, 240 insertions(+), 6 deletions(-) create mode 100644 modules/openapi-generator/src/test/resources/3_0/swift/recursive-models.yaml diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/Swift5ClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/Swift5ClientCodegen.java index 92224fc5a338..54563831a40a 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/Swift5ClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/Swift5ClientCodegen.java @@ -746,6 +746,7 @@ private static List splitAdditionalModelOption(String value) { @Override public Map postProcessAllModels(Map objs) { objs = super.postProcessAllModels(objs); + markModelClassRendering(objs); if (additionalModelObjectAttributes.isEmpty() && additionalModelEnumAttributes.isEmpty() && additionalModelImports.isEmpty()) { @@ -766,6 +767,62 @@ public Map postProcessAllModels(Map objs) return objs; } + /** Models on an inline reference cycle become final classes: a struct that stores itself has infinite size (#15240). */ + private void markModelClassRendering(Map objs) { + Map modelsByClassname = new HashMap<>(); + for (ModelsMap modelsMap : objs.values()) { + for (ModelMap modelMap : modelsMap.getModels()) { + CodegenModel cm = modelMap.getModel(); + modelsByClassname.put(cm.classname, cm); + } + } + + Map> inlineRefs = new HashMap<>(); + for (CodegenModel cm : modelsByClassname.values()) { + Set refs = new LinkedHashSet<>(); + collectInlineModelRefs(cm.allVars, modelsByClassname, refs); + if (cm.getComposedSchemas() != null) { + // oneOf/anyOf render as enums with inline associated values, so they carry the + // recursion; allOf is flattened into allVars and is deliberately not an edge + collectInlineModelRefs(cm.getComposedSchemas().getOneOf(), modelsByClassname, refs); + collectInlineModelRefs(cm.getComposedSchemas().getAnyOf(), modelsByClassname, refs); + } + inlineRefs.put(cm.classname, refs); + } + + for (CodegenModel cm : modelsByClassname.values()) { + if (useClasses || isOnInlineReferenceCycle(cm.classname, inlineRefs)) { + cm.vendorExtensions.put("x-swift-use-class", true); + } + } + } + + private void collectInlineModelRefs(List vars, Map modelsByClassname, Set refs) { + if (vars == null) { + return; + } + for (CodegenProperty var : vars) { + if (!var.isContainer && var.complexType != null && modelsByClassname.containsKey(var.complexType)) { + refs.add(var.complexType); + } + } + } + + private boolean isOnInlineReferenceCycle(String classname, Map> inlineRefs) { + Deque toVisit = new ArrayDeque<>(inlineRefs.getOrDefault(classname, Collections.emptySet())); + Set visited = new HashSet<>(); + while (!toVisit.isEmpty()) { + String current = toVisit.pop(); + if (classname.equals(current)) { + return true; + } + if (visited.add(current)) { + toVisit.addAll(inlineRefs.getOrDefault(current, Collections.emptySet())); + } + } + return false; + } + @Override protected boolean isReservedWord(String word) { return word != null && reservedWords.contains(word); //don't lowercase as super does diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/Swift6ClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/Swift6ClientCodegen.java index 77394f42b70f..2abc8fb13c82 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/Swift6ClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/Swift6ClientCodegen.java @@ -794,6 +794,7 @@ private static List splitAdditionalModelOption(String value) { @Override public Map postProcessAllModels(Map objs) { objs = super.postProcessAllModels(objs); + markModelClassRendering(objs); if (additionalModelObjectAttributes.isEmpty() && additionalModelEnumAttributes.isEmpty() && additionalModelImports.isEmpty()) { @@ -814,6 +815,69 @@ public Map postProcessAllModels(Map objs) return objs; } + /** Models on an inline reference cycle become final classes: a struct that stores itself has infinite size (#15240). */ + private void markModelClassRendering(Map objs) { + Map modelsByClassname = new HashMap<>(); + for (ModelsMap modelsMap : objs.values()) { + for (ModelMap modelMap : modelsMap.getModels()) { + CodegenModel cm = modelMap.getModel(); + modelsByClassname.put(cm.classname, cm); + } + } + + Map> inlineRefs = new HashMap<>(); + for (CodegenModel cm : modelsByClassname.values()) { + Set refs = new LinkedHashSet<>(); + collectInlineModelRefs(cm.allVars, modelsByClassname, refs); + if (cm.getComposedSchemas() != null) { + // oneOf/anyOf render as enums with inline associated values, so they carry the + // recursion; allOf is flattened into allVars and is deliberately not an edge + collectInlineModelRefs(cm.getComposedSchemas().getOneOf(), modelsByClassname, refs); + collectInlineModelRefs(cm.getComposedSchemas().getAnyOf(), modelsByClassname, refs); + } + inlineRefs.put(cm.classname, refs); + } + + for (CodegenModel cm : modelsByClassname.values()) { + boolean recursive = !useClasses && isOnInlineReferenceCycle(cm.classname, inlineRefs); + if (useClasses || recursive) { + cm.vendorExtensions.put("x-swift-use-class", true); + } + if ((useClasses && readonlyProperties) || recursive) { + // a struct embedding one of these classes is still declared Sendable, so the + // class must conform; a recursion-breaking class is treated as unchecked the + // way readonlyProperties classes already are + cm.vendorExtensions.put("x-swift-unchecked-sendable", true); + } + } + } + + private void collectInlineModelRefs(List vars, Map modelsByClassname, Set refs) { + if (vars == null) { + return; + } + for (CodegenProperty var : vars) { + if (!var.isContainer && var.complexType != null && modelsByClassname.containsKey(var.complexType)) { + refs.add(var.complexType); + } + } + } + + private boolean isOnInlineReferenceCycle(String classname, Map> inlineRefs) { + Deque toVisit = new ArrayDeque<>(inlineRefs.getOrDefault(classname, Collections.emptySet())); + Set visited = new HashSet<>(); + while (!toVisit.isEmpty()) { + String current = toVisit.pop(); + if (classname.equals(current)) { + return true; + } + if (visited.add(current)) { + toVisit.addAll(inlineRefs.getOrDefault(current, Collections.emptySet())); + } + } + return false; + } + @Override protected boolean isReservedWord(String word) { return word != null && reservedWords.contains(word); //don't lowercase as super does diff --git a/modules/openapi-generator/src/main/resources/swift5/modelObject.mustache b/modules/openapi-generator/src/main/resources/swift5/modelObject.mustache index b6759a6a0e4b..7e107cb08f5c 100644 --- a/modules/openapi-generator/src/main/resources/swift5/modelObject.mustache +++ b/modules/openapi-generator/src/main/resources/swift5/modelObject.mustache @@ -1,5 +1,5 @@ {{#additionalModelObjectAttributes}}{{{.}}} -{{/additionalModelObjectAttributes}}{{^objcCompatible}}{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} {{#useClasses}}final class{{/useClasses}}{{^useClasses}}struct{{/useClasses}} {{{classname}}}: {{#useVapor}}Content{{/useVapor}}{{^useVapor}}Codable{{#useJsonEncodable}}, JSONEncodable{{/useJsonEncodable}}{{/useVapor}}{{#vendorExtensions.x-swift-hashable}}, Hashable{{/vendorExtensions.x-swift-hashable}} { +{{/additionalModelObjectAttributes}}{{^objcCompatible}}{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} {{#vendorExtensions.x-swift-use-class}}final class{{/vendorExtensions.x-swift-use-class}}{{^vendorExtensions.x-swift-use-class}}struct{{/vendorExtensions.x-swift-use-class}} {{{classname}}}: {{#useVapor}}Content{{/useVapor}}{{^useVapor}}Codable{{#useJsonEncodable}}, JSONEncodable{{/useJsonEncodable}}{{/useVapor}}{{#vendorExtensions.x-swift-hashable}}, Hashable{{/vendorExtensions.x-swift-hashable}} { {{/objcCompatible}}{{#objcCompatible}}@objcMembers {{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} class {{classname}}: NSObject, Codable{{#useJsonEncodable}}, JSONEncodable{{/useJsonEncodable}} { {{/objcCompatible}} @@ -121,7 +121,7 @@ {{/allVars}} let additionalPropertiesContainer = try decoder.container(keyedBy: String.self) additionalProperties = try additionalPropertiesContainer.decodeMap({{{additionalPropertiesType}}}.self, excludedKeys: nonAdditionalPropertyKeys) - }{{/additionalPropertiesType}}{{/generateModelAdditionalProperties}}{{^objcCompatible}}{{#useClasses}}{{#vendorExtensions.x-swift-hashable}} + }{{/additionalPropertiesType}}{{/generateModelAdditionalProperties}}{{^objcCompatible}}{{#vendorExtensions.x-swift-use-class}}{{#vendorExtensions.x-swift-hashable}} {{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} static func == (lhs: {{classname}}, rhs: {{classname}}) -> Bool { {{#allVars}} @@ -135,5 +135,5 @@ hasher.combine({{{name}}}{{^vendorExtensions.x-null-encodable}}{{^required}}?{{/required}}{{/vendorExtensions.x-null-encodable}}.hashValue) {{/allVars}} {{#generateModelAdditionalProperties}}{{#additionalPropertiesType}}hasher.combine(additionalProperties.hashValue){{/additionalPropertiesType}}{{/generateModelAdditionalProperties}} - }{{/vendorExtensions.x-swift-hashable}}{{/useClasses}}{{/objcCompatible}} + }{{/vendorExtensions.x-swift-hashable}}{{/vendorExtensions.x-swift-use-class}}{{/objcCompatible}} } diff --git a/modules/openapi-generator/src/main/resources/swift6/modelObject.mustache b/modules/openapi-generator/src/main/resources/swift6/modelObject.mustache index c4677923979d..445826d40146 100644 --- a/modules/openapi-generator/src/main/resources/swift6/modelObject.mustache +++ b/modules/openapi-generator/src/main/resources/swift6/modelObject.mustache @@ -1,5 +1,5 @@ {{#additionalModelObjectAttributes}}{{{.}}} -{{/additionalModelObjectAttributes}}{{^objcCompatible}}{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} {{#useClasses}}final class{{/useClasses}}{{^useClasses}}struct{{/useClasses}} {{{classname}}}: {{^useClasses}}Sendable, {{/useClasses}}{{#useClasses}}{{#readonlyProperties}}@unchecked Sendable, {{/readonlyProperties}}{{/useClasses}}{{#useVapor}}Content{{/useVapor}}{{^useVapor}}Codable{{/useVapor}}{{#vendorExtensions.x-swift-hashable}}, Hashable{{/vendorExtensions.x-swift-hashable}} { +{{/additionalModelObjectAttributes}}{{^objcCompatible}}{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} {{#vendorExtensions.x-swift-use-class}}final class{{/vendorExtensions.x-swift-use-class}}{{^vendorExtensions.x-swift-use-class}}struct{{/vendorExtensions.x-swift-use-class}} {{{classname}}}: {{^vendorExtensions.x-swift-use-class}}Sendable, {{/vendorExtensions.x-swift-use-class}}{{#vendorExtensions.x-swift-unchecked-sendable}}@unchecked Sendable, {{/vendorExtensions.x-swift-unchecked-sendable}}{{#useVapor}}Content{{/useVapor}}{{^useVapor}}Codable{{/useVapor}}{{#vendorExtensions.x-swift-hashable}}, Hashable{{/vendorExtensions.x-swift-hashable}} { {{/objcCompatible}}{{#objcCompatible}}@objcMembers {{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} final class {{classname}}: NSObject, Codable, @unchecked Sendable { {{/objcCompatible}} @@ -121,7 +121,7 @@ {{/allVars}} let additionalPropertiesContainer = try decoder.container(keyedBy: String.self) additionalProperties = try additionalPropertiesContainer.decodeMap({{{additionalPropertiesType}}}.self, excludedKeys: nonAdditionalPropertyKeys) - }{{/additionalPropertiesType}}{{/generateModelAdditionalProperties}}{{^objcCompatible}}{{#useClasses}}{{#vendorExtensions.x-swift-hashable}} + }{{/additionalPropertiesType}}{{/generateModelAdditionalProperties}}{{^objcCompatible}}{{#vendorExtensions.x-swift-use-class}}{{#vendorExtensions.x-swift-hashable}} {{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} static func == (lhs: {{classname}}, rhs: {{classname}}) -> Bool { {{#allVars}} @@ -135,5 +135,5 @@ hasher.combine({{{name}}}{{^vendorExtensions.x-null-encodable}}{{^required}}?{{/required}}{{/vendorExtensions.x-null-encodable}}.hashValue) {{/allVars}} {{#generateModelAdditionalProperties}}{{#additionalPropertiesType}}hasher.combine(additionalProperties.hashValue){{/additionalPropertiesType}}{{/generateModelAdditionalProperties}} - }{{/vendorExtensions.x-swift-hashable}}{{/useClasses}}{{/objcCompatible}} + }{{/vendorExtensions.x-swift-hashable}}{{/vendorExtensions.x-swift-use-class}}{{/objcCompatible}} } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/swift5/Swift5ClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/swift5/Swift5ClientCodegenTest.java index 782a5d95d57e..6085915f86c5 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/swift5/Swift5ClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/swift5/Swift5ClientCodegenTest.java @@ -266,6 +266,31 @@ public void testPodAuthors() throws Exception { Assert.assertEquals(podAuthors, openAPIDevs); } + @Test(description = "models on an inline reference cycle become classes, everything else stays a struct") + public void testRecursiveModelsBecomeClasses() throws IOException { + Path target = Files.createTempDirectory("test"); + target.toFile().deleteOnExit(); + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("swift5") + .setInputSpec("src/test/resources/3_0/swift/recursive-models.yaml") + .setOutputDir(target.toAbsolutePath().toString()); + new DefaultGenerator().opts(configurator.toClientOptInput()).generate(); + + Path models = target.resolve("OpenAPIClient/Classes/OpenAPIs/Models"); + // a struct that stores itself inline has infinite size and does not compile (#15240): + // the self-referencing model and both halves of the mutual cycle become final classes + TestUtils.assertFileContains(models.resolve("ContactInfo.swift"), "public final class ContactInfo:"); + TestUtils.assertFileContains(models.resolve("NodeA.swift"), "public final class NodeA:"); + TestUtils.assertFileContains(models.resolve("NodeB.swift"), "public final class NodeB:"); + // embedding a cyclic class costs nothing, and containers already give heap + // indirection - these stay structs + TestUtils.assertFileContains(models.resolve("DomainInfo.swift"), "public struct DomainInfo:"); + TestUtils.assertFileContains(models.resolve("Category.swift"), "public struct Category:"); + // allOf is flattened rather than stored, so it is not an edge: only Derived is on a cycle + TestUtils.assertFileContains(models.resolve("Derived.swift"), "public final class Derived:"); + TestUtils.assertFileContains(models.resolve("Base.swift"), "public struct Base:"); + } + @Test(description = "Bug example code generation", enabled = true) public void crashSwift5ExampleCodeGenerationStackOverflowTest() throws IOException { //final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/bugs/Swift5CodeGenerationStackOverflow#2966.yaml"); diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/swift6/Swift6ClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/swift6/Swift6ClientCodegenTest.java index 3fe2f07d3b0f..27c6f151381e 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/swift6/Swift6ClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/swift6/Swift6ClientCodegenTest.java @@ -52,6 +52,31 @@ public void testToRegularExpressionRemainsValidInSwiftStringLiteral() throws Exc Assert.assertEquals(swiftCodegen.toRegularExpression("/[a-z]/i"), "/[a-z]/i"); } + @Test(description = "models on an inline reference cycle become classes, everything else stays a struct") + public void testRecursiveModelsBecomeClasses() throws IOException { + Path target = Files.createTempDirectory("test"); + target.toFile().deleteOnExit(); + final CodegenConfigurator configurator = new CodegenConfigurator() + .setGeneratorName("swift6") + .setInputSpec("src/test/resources/3_0/swift/recursive-models.yaml") + .setOutputDir(target.toAbsolutePath().toString()); + new DefaultGenerator().opts(configurator.toClientOptInput()).generate(); + + Path models = target.resolve("Sources/OpenAPIClient/Models"); + // a struct that stores itself inline has infinite size and does not compile (#15240): + // the self-referencing model and both halves of the mutual cycle become final classes + TestUtils.assertFileContains(models.resolve("ContactInfo.swift"), "public final class ContactInfo: @unchecked Sendable,"); + TestUtils.assertFileContains(models.resolve("NodeA.swift"), "public final class NodeA: @unchecked Sendable,"); + TestUtils.assertFileContains(models.resolve("NodeB.swift"), "public final class NodeB: @unchecked Sendable,"); + // embedding a cyclic class costs nothing, and containers already give heap + // indirection - these stay structs + TestUtils.assertFileContains(models.resolve("DomainInfo.swift"), "public struct DomainInfo: Sendable,"); + TestUtils.assertFileContains(models.resolve("Category.swift"), "public struct Category: Sendable,"); + // allOf is flattened rather than stored, so it is not an edge: only Derived is on a cycle + TestUtils.assertFileContains(models.resolve("Derived.swift"), "public final class Derived: @unchecked Sendable,"); + TestUtils.assertFileContains(models.resolve("Base.swift"), "public struct Base: Sendable,"); + } + @Test(enabled = true) public void testCapitalizedReservedWord() throws Exception { Assert.assertEquals(swiftCodegen.toEnumVarName("AS", null), "_as"); diff --git a/modules/openapi-generator/src/test/resources/3_0/swift/recursive-models.yaml b/modules/openapi-generator/src/test/resources/3_0/swift/recursive-models.yaml new file mode 100644 index 000000000000..82493bce27fb --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/swift/recursive-models.yaml @@ -0,0 +1,63 @@ +openapi: 3.0.3 +info: + title: recursive models + version: 1.0.0 +paths: + /contacts: + get: + operationId: getContact + responses: + '200': + description: ok + content: + application/json: + schema: + $ref: '#/components/schemas/DomainInfo' +components: + schemas: + ContactInfo: + type: object + properties: + name: + type: string + internationalizedPostalInfo: + $ref: '#/components/schemas/ContactInfo' + DomainInfo: + type: object + properties: + domainName: + type: string + registrant: + $ref: '#/components/schemas/ContactInfo' + NodeA: + type: object + properties: + b: + $ref: '#/components/schemas/NodeB' + NodeB: + type: object + properties: + a: + $ref: '#/components/schemas/NodeA' + Category: + type: object + properties: + name: + type: string + children: + type: array + items: + $ref: '#/components/schemas/Category' + # allOf is flattened, not stored: Derived cycles through its inherited `related`, Base does not + Base: + type: object + properties: + related: + $ref: '#/components/schemas/Derived' + Derived: + allOf: + - $ref: '#/components/schemas/Base' + - type: object + properties: + extra: + type: string