Skip to content

Commit f48e736

Browse files
fix(typescript): serialize JSON header parameter content (#25088)
1 parent 5cfc36b commit f48e736

10 files changed

Lines changed: 353 additions & 12 deletions

File tree

‎modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenParameter.java‎

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ public class CodegenParameter implements IJsonSchemaValidationProperties {
5050
* If a query parameter should be serialized as json
5151
*/
5252
public boolean queryIsJsonMimeType;
53+
/**
54+
* True when a header parameter uses a JSON media type in Parameter.content.
55+
*/
56+
public boolean headerIsJsonMimeType;
5357
/**
5458
* datatype is the generic inner parameter of a std::optional for C++, or Optional (Java)
5559
*/
@@ -270,6 +274,7 @@ public CodegenParameter copy() {
270274
output.isArray = this.isArray;
271275
output.isMap = this.isMap;
272276
output.queryIsJsonMimeType = this.queryIsJsonMimeType;
277+
output.headerIsJsonMimeType = this.headerIsJsonMimeType;
273278
output.isOptional = this.isOptional;
274279
output.isExplode = this.isExplode;
275280
output.style = this.style;
@@ -294,15 +299,15 @@ public int hashCode() {
294299
isFormStyle, isSpaceDelimited, isPipeDelimited,
295300
jsonSchema, isString, isNumeric, isInteger, isLong, isNumber, isFloat, isDouble, isDecimal,
296301
isByteArray, isBinary, isBoolean, isDate, isDateTime, isUuid, isUri, isEmail, isPassword,
297-
isFreeFormObject, isAnyType, isArray, isMap, queryIsJsonMimeType, isOptional, isFile, isEnum, isEnumRef, _enum, allowableValues,
298-
items, mostInnerItems, additionalProperties, vars, requiredVars, vendorExtensions, hasValidation,
299-
getMaxProperties(), getMinProperties(), isNullable, isDeprecated, required, getMaximum(),
300-
getExclusiveMaximum(), getMinimum(), getExclusiveMinimum(), getMaxLength(), getMinLength(),
301-
getPattern(), getMaxItems(), getMinItems(), getUniqueItems(), contentType, multipleOf, isNull, isVoid,
302-
additionalPropertiesIsAnyType, hasVars, hasRequired, isShort, isUnboundedInteger,
303-
hasDiscriminatorWithNonEmptyMapping, composedSchemas, hasMultipleTypes, schema, content,
304-
requiredVarsMap, ref, uniqueItemsBoolean, schemaIsFromAdditionalProperties,
305-
nameInPascalCase, nameInCamelCase, nameInLowerCase, nameInSnakeCase);
302+
isFreeFormObject, isAnyType, isArray, isMap, queryIsJsonMimeType, headerIsJsonMimeType, isOptional,
303+
isFile, isEnum, isEnumRef, _enum, allowableValues, items, mostInnerItems, additionalProperties, vars,
304+
requiredVars, vendorExtensions, hasValidation, getMaxProperties(), getMinProperties(), isNullable,
305+
isDeprecated, required, getMaximum(), getExclusiveMaximum(), getMinimum(), getExclusiveMinimum(),
306+
getMaxLength(), getMinLength(), getPattern(), getMaxItems(), getMinItems(), getUniqueItems(),
307+
contentType, multipleOf, isNull, isVoid, additionalPropertiesIsAnyType, hasVars, hasRequired, isShort,
308+
isUnboundedInteger, hasDiscriminatorWithNonEmptyMapping, composedSchemas, hasMultipleTypes, schema,
309+
content, requiredVarsMap, ref, uniqueItemsBoolean, schemaIsFromAdditionalProperties, nameInPascalCase,
310+
nameInCamelCase, nameInLowerCase, nameInSnakeCase);
306311
}
307312

308313
@Override
@@ -345,6 +350,7 @@ public boolean equals(Object o) {
345350
isArray == that.isArray &&
346351
isMap == that.isMap &&
347352
queryIsJsonMimeType == that.queryIsJsonMimeType &&
353+
headerIsJsonMimeType == that.headerIsJsonMimeType &&
348354
isOptional == that.isOptional &&
349355
isFile == that.isFile &&
350356
isEnum == that.isEnum &&
@@ -486,6 +492,7 @@ public String toString() {
486492
sb.append(", isArray=").append(isArray);
487493
sb.append(", isMap=").append(isMap);
488494
sb.append(", queryIsJsonMimeType=").append(queryIsJsonMimeType);
495+
sb.append(", headerIsJsonMimeType=").append(headerIsJsonMimeType);
489496
sb.append(", isOptional=").append(isOptional);
490497
sb.append(", isFile=").append(isFile);
491498
sb.append(", isEnum=").append(isEnum);

‎modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5722,6 +5722,8 @@ public CodegenParameter fromParameter(Parameter parameter, Set<String> imports)
57225722
codegenParameter.isPathParam = true;
57235723
} else if (parameter instanceof HeaderParameter || "header".equalsIgnoreCase(parameter.getIn())) {
57245724
codegenParameter.isHeaderParam = true;
5725+
codegenParameter.headerIsJsonMimeType = isJsonMimeType(codegenParameter.contentType)
5726+
|| isJsonVendorMimeType(codegenParameter.contentType);
57255727
} else if (parameter instanceof CookieParameter || "cookie".equalsIgnoreCase(parameter.getIn())) {
57265728
codegenParameter.isCookieParam = true;
57275729
} else {

‎modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptFetchClientCodegen.java‎

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@
1919

2020
import com.google.common.collect.ImmutableMap;
2121
import com.samskivert.mustache.Mustache;
22+
import io.swagger.v3.oas.models.OpenAPI;
2223
import io.swagger.v3.oas.models.Operation;
24+
import io.swagger.v3.oas.models.PathItem;
2325
import io.swagger.v3.oas.models.media.Schema;
26+
import io.swagger.v3.oas.models.parameters.HeaderParameter;
2427
import io.swagger.v3.oas.models.parameters.Parameter;
2528
import io.swagger.v3.oas.models.parameters.RequestBody;
2629
import io.swagger.v3.oas.models.responses.ApiResponse;
@@ -34,6 +37,7 @@
3437
import org.openapitools.codegen.model.ModelMap;
3538
import org.openapitools.codegen.model.ModelsMap;
3639
import org.openapitools.codegen.model.OperationsMap;
40+
import org.openapitools.codegen.model.WebhooksMap;
3741
import org.openapitools.codegen.templating.mustache.IndentedLambda;
3842
import org.openapitools.codegen.utils.ModelUtils;
3943
import org.slf4j.Logger;
@@ -355,6 +359,7 @@ public void processOpts() {
355359
if (!withoutRuntimeChecks) {
356360
this.modelTemplateFiles.put("models.mustache", ".ts");
357361
}
362+
additionalProperties.put("modelSerializersEnabled", !withoutRuntimeChecks);
358363

359364
// `date` needs the model (de)serialization to convert with, which
360365
// withoutRuntimeChecks removes: the raw string would just be cast to Date.
@@ -708,7 +713,81 @@ public List<CodegenParameter> fromRequestBodyToFormParameters(RequestBody body,
708713
@Override
709714
public ExtendedCodegenParameter fromParameter(Parameter parameter, Set<String> imports) {
710715
CodegenParameter cp = super.fromParameter(parameter, imports);
711-
return new ExtendedCodegenParameter(cp);
716+
ExtendedCodegenParameter result = new ExtendedCodegenParameter(cp);
717+
Parameter resolvedParameter = ModelUtils.getReferencedParameter(openAPI, parameter);
718+
result.jsonHeaderUsesModelSerializer = cp.headerIsJsonMimeType && cp.isModel
719+
&& hasGeneratedModelHeaderSchema(openAPI, resolvedParameter);
720+
return result;
721+
}
722+
723+
@Override
724+
public void processOpenAPI(OpenAPI openAPI) {
725+
super.processOpenAPI(openAPI);
726+
if (!withoutRuntimeChecks) {
727+
return;
728+
}
729+
if (openAPI.getPaths() != null) {
730+
for (PathItem path : openAPI.getPaths().values()) {
731+
if (hasJsonContentModelHeader(openAPI, path)) {
732+
enableJsonHeaderModelSerializers();
733+
return;
734+
}
735+
}
736+
}
737+
if (openAPI.getWebhooks() != null) {
738+
for (PathItem webhook : openAPI.getWebhooks().values()) {
739+
if (hasJsonContentModelHeader(openAPI, webhook)) {
740+
enableJsonHeaderModelSerializers();
741+
return;
742+
}
743+
}
744+
}
745+
}
746+
747+
private boolean hasJsonContentModelHeader(OpenAPI openAPI, PathItem path) {
748+
if (hasJsonContentModelHeader(openAPI, path.getParameters())) {
749+
return true;
750+
}
751+
for (Operation operation : path.readOperations()) {
752+
if (hasJsonContentModelHeader(openAPI, operation.getParameters())) {
753+
return true;
754+
}
755+
}
756+
return false;
757+
}
758+
759+
private boolean hasJsonContentModelHeader(OpenAPI openAPI, List<Parameter> parameters) {
760+
return parameters != null && parameters.stream()
761+
.map(parameter -> ModelUtils.getReferencedParameter(openAPI, parameter))
762+
.anyMatch(parameter -> isJsonContentHeader(parameter)
763+
&& hasGeneratedModelHeaderSchema(openAPI, parameter));
764+
}
765+
766+
private boolean isJsonContentHeader(Parameter parameter) {
767+
if (parameter == null
768+
|| !(parameter instanceof HeaderParameter || "header".equalsIgnoreCase(parameter.getIn()))
769+
|| parameter.getSchema() != null
770+
|| parameter.getContent() == null
771+
|| parameter.getContent().isEmpty()) {
772+
return false;
773+
}
774+
String contentType = parameter.getContent().keySet().iterator().next();
775+
return isJsonMimeType(contentType) || isJsonVendorMimeType(contentType);
776+
}
777+
778+
private boolean hasGeneratedModelHeaderSchema(OpenAPI openAPI, Parameter parameter) {
779+
if (parameter.getContent() == null || parameter.getContent().isEmpty()) {
780+
return false;
781+
}
782+
Schema schema = parameter.getContent().values().iterator().next().getSchema();
783+
return schema != null && schema.get$ref() != null
784+
&& ModelUtils.isModel(ModelUtils.getReferencedSchema(openAPI, schema));
785+
}
786+
787+
private void enableJsonHeaderModelSerializers() {
788+
modelTemplateFiles.put("models.mustache", ".ts");
789+
additionalProperties.put("jsonHeaderModelSerializers", true);
790+
additionalProperties.put("modelSerializersEnabled", true);
712791
}
713792

714793
@Override
@@ -875,6 +954,20 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap operations, L
875954
return operations;
876955
}
877956

957+
@Override
958+
public WebhooksMap postProcessWebhooksWithModels(WebhooksMap webhooks, List<ModelMap> allModels) {
959+
if (webhooks.getWebhooks().getOperation().stream()
960+
.anyMatch(operation -> operation.headerParams != null && operation.headerParams.stream()
961+
.anyMatch(parameter -> ((ExtendedCodegenParameter) parameter).jsonHeaderUsesModelSerializer))) {
962+
for (Map<String, String> im : webhooks.getImports()) {
963+
String className = im.get("import").replace(modelPackage() + ".", "");
964+
im.put("className", className);
965+
im.put("classFileName", convertUsingFileNamingConvention(className));
966+
}
967+
}
968+
return webhooks;
969+
}
970+
878971
@Override
879972
public Map<String, Object> postProcessSupportingFileData(Map<String, Object> objs) {
880973
Map<String, Object> parentObjs = super.postProcessSupportingFileData(objs);
@@ -1544,6 +1637,7 @@ private static String getItemsDataType(CodegenProperty items) {
15441637

15451638
public class ExtendedCodegenParameter extends CodegenParameter {
15461639
public String dataTypeAlternate;
1640+
public boolean jsonHeaderUsesModelSerializer;
15471641
public boolean isUniqueId; // this parameter represents a unique id (x-isUniqueId: true)
15481642
public List<CodegenProperty> readOnlyVars; // a list of read-only properties
15491643
public boolean hasReadOnly = false; // indicates the type has at least one read-only property
@@ -1572,6 +1666,7 @@ public ExtendedCodegenParameter(CodegenParameter cp) {
15721666
this.isQueryParam = cp.isQueryParam;
15731667
this.isPathParam = cp.isPathParam;
15741668
this.isHeaderParam = cp.isHeaderParam;
1669+
this.headerIsJsonMimeType = cp.headerIsJsonMimeType;
15751670
this.isCookieParam = cp.isCookieParam;
15761671
this.isBodyParam = cp.isBodyParam;
15771672
this.isContainer = cp.isContainer;
@@ -1660,6 +1755,7 @@ public ExtendedCodegenParameter copy() {
16601755
CodegenParameter superCopy = super.copy();
16611756
ExtendedCodegenParameter output = new ExtendedCodegenParameter(superCopy);
16621757
output.dataTypeAlternate = this.dataTypeAlternate;
1758+
output.jsonHeaderUsesModelSerializer = this.jsonHeaderUsesModelSerializer;
16631759
output.isUniqueId = this.isUniqueId;
16641760
return output;
16651761
}

‎modules/openapi-generator/src/main/resources/typescript-fetch/apis.mustache‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ import type {
99
{{className}},
1010
{{/imports}}
1111
} from '../models/index{{importFileExtension}}';
12+
{{#jsonHeaderModelSerializers}}
13+
{{#imports}}
14+
import { {{className}}ToJSON } from '../models/{{classFileName}}{{importFileExtension}}';
15+
{{/imports}}
16+
{{/jsonHeaderModelSerializers}}
1217
{{/withoutRuntimeChecks}}
1318
{{^withoutRuntimeChecks}}
1419
{{#imports}}
@@ -250,6 +255,15 @@ export class {{classname}} extends runtime.BaseAPI {
250255
{{/bodyParam}}
251256
{{/hasContentTypeRequestVariants}}
252257
{{#headerParams}}
258+
{{#headerIsJsonMimeType}}
259+
{{#jsonHeaderUsesModelSerializer}}
260+
if (requestParameters['{{paramName}}'] !== undefined) {
261+
headerParameters['{{baseName}}'] = JSON.stringify({{dataType}}ToJSON(requestParameters['{{paramName}}']));
262+
}
263+
264+
{{/jsonHeaderUsesModelSerializer}}
265+
{{/headerIsJsonMimeType}}
266+
{{^jsonHeaderUsesModelSerializer}}
253267
{{#isArray}}
254268
if (requestParameters['{{paramName}}'] != null) {
255269
headerParameters['{{baseName}}'] = {{#uniqueItems}}Array.from({{/uniqueItems}}requestParameters['{{paramName}}']{{#uniqueItems}}){{/uniqueItems}}!.join(runtime.COLLECTION_FORMATS["{{collectionFormat}}"]);
@@ -262,6 +276,7 @@ export class {{classname}} extends runtime.BaseAPI {
262276
}
263277
264278
{{/isArray}}
279+
{{/jsonHeaderUsesModelSerializer}}
265280
{{/headerParams}}
266281
{{#authMethods}}
267282
{{#isBasic}}

‎modules/openapi-generator/src/main/resources/typescript-fetch/runtime.mustache‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,7 @@ export function parseDateTime(value: any): Date {
452452
{{/isDateLibraryDate}}
453453
{{/isDateLibraryTemporal}}
454454

455-
{{^withoutRuntimeChecks}}
455+
{{#modelSerializersEnabled}}
456456
export function mapValues(data: any, fn: (item: any) => any) {
457457
const result: { [key: string]: any } = {};
458458
for (const key of Object.keys(data)) {
@@ -465,7 +465,7 @@ export function mapValues(data: any, fn: (item: any) => any) {
465465
export function anyToJSON(value: any): any {
466466
return value;
467467
}
468-
{{/withoutRuntimeChecks}}
468+
{{/modelSerializersEnabled}}
469469

470470
export function canConsumeForm(consumes: Consume[]): boolean {
471471
for (const consume of consumes) {

‎modules/openapi-generator/src/main/resources/typescript-node/api-single.mustache‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,14 @@ export class {{classname}} {
190190

191191
{{/queryParams}}
192192
{{#headerParams}}
193+
{{#headerIsJsonMimeType}}
194+
if ({{paramName}} !== undefined) {
195+
localVarHeaderParams['{{baseName}}'] = JSON.stringify(ObjectSerializer.serialize({{paramName}}, "{{{dataType}}}"));
196+
}
197+
{{/headerIsJsonMimeType}}
198+
{{^headerIsJsonMimeType}}
193199
localVarHeaderParams['{{baseName}}'] = ObjectSerializer.serialize({{paramName}}, "{{{dataType}}}");
200+
{{/headerIsJsonMimeType}}
194201
{{/headerParams}}
195202
(<any>Object).assign(localVarHeaderParams, options.headers);
196203

0 commit comments

Comments
 (0)