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/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/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/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/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/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/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/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 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()); + } } 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 }); + } } }