Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ public class CodegenParameter implements IJsonSchemaValidationProperties {
* If a query parameter should be serialized as json
*/
public boolean queryIsJsonMimeType;
/**
* True when a header parameter uses a JSON media type in Parameter.content.
*/
public boolean headerIsJsonMimeType;
/**
* datatype is the generic inner parameter of a std::optional for C++, or Optional (Java)
*/
Expand Down Expand Up @@ -270,6 +274,7 @@ public CodegenParameter copy() {
output.isArray = this.isArray;
output.isMap = this.isMap;
output.queryIsJsonMimeType = this.queryIsJsonMimeType;
output.headerIsJsonMimeType = this.headerIsJsonMimeType;
output.isOptional = this.isOptional;
output.isExplode = this.isExplode;
output.style = this.style;
Expand All @@ -294,7 +299,7 @@ public int hashCode() {
isFormStyle, isSpaceDelimited, isPipeDelimited,
jsonSchema, isString, isNumeric, isInteger, isLong, isNumber, isFloat, isDouble, isDecimal,
isByteArray, isBinary, isBoolean, isDate, isDateTime, isUuid, isUri, isEmail, isPassword,
isFreeFormObject, isAnyType, isArray, isMap, queryIsJsonMimeType, isOptional, isFile, isEnum, isEnumRef, _enum, allowableValues,
isFreeFormObject, isAnyType, isArray, isMap, queryIsJsonMimeType, headerIsJsonMimeType, isOptional, isFile, isEnum, isEnumRef, _enum, allowableValues,
items, mostInnerItems, additionalProperties, vars, requiredVars, vendorExtensions, hasValidation,
getMaxProperties(), getMinProperties(), isNullable, isDeprecated, required, getMaximum(),
getExclusiveMaximum(), getMinimum(), getExclusiveMinimum(), getMaxLength(), getMinLength(),
Expand Down Expand Up @@ -345,6 +350,7 @@ public boolean equals(Object o) {
isArray == that.isArray &&
isMap == that.isMap &&
queryIsJsonMimeType == that.queryIsJsonMimeType &&
headerIsJsonMimeType == that.headerIsJsonMimeType &&
isOptional == that.isOptional &&
isFile == that.isFile &&
isEnum == that.isEnum &&
Expand Down Expand Up @@ -486,6 +492,7 @@ public String toString() {
sb.append(", isArray=").append(isArray);
sb.append(", isMap=").append(isMap);
sb.append(", queryIsJsonMimeType=").append(queryIsJsonMimeType);
sb.append(", headerIsJsonMimeType=").append(headerIsJsonMimeType);
sb.append(", isOptional=").append(isOptional);
sb.append(", isFile=").append(isFile);
sb.append(", isEnum=").append(isEnum);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5722,6 +5722,8 @@ public CodegenParameter fromParameter(Parameter parameter, Set<String> imports)
codegenParameter.isPathParam = true;
} else if (parameter instanceof HeaderParameter || "header".equalsIgnoreCase(parameter.getIn())) {
codegenParameter.isHeaderParam = true;
codegenParameter.headerIsJsonMimeType = isJsonMimeType(codegenParameter.contentType)
|| isJsonVendorMimeType(codegenParameter.contentType);
} else if (parameter instanceof CookieParameter || "cookie".equalsIgnoreCase(parameter.getIn())) {
codegenParameter.isCookieParam = true;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,34 @@ namespace {{packageName}}.Client
return obj != null ? Newtonsoft.Json.JsonConvert.SerializeObject(obj) : null;
}

/// <summary>
/// Serializes the given object as an ASCII-safe JSON string. Used to serialize the value of
/// a header parameter whose content type is JSON (e.g. <c>content: application/json</c>),
/// so model objects are rendered as JSON instead of their debug ToString() representation.
/// </summary>
/// <param name="obj">The object to serialize.</param>
/// <returns>JSON representation of the object with every C0 control character (including CR and LF), DEL, and non-ASCII character escaped as \uXXXX, so raw control bytes can never reach the header value.</returns>
public static string ParameterToJsonString(object obj)
{
// JsonConvert renders a null reference as the JSON literal "null" (not a null string).
string json = Newtonsoft.Json.JsonConvert.SerializeObject(obj);

// Escape everything that is not printable ASCII: C0 controls (including CR and LF), DEL,
// and all non-ASCII characters, as \uXXXX. Because this runs over the final serialized
// string, the result is safe to use as an HTTP header value even if a custom converter
// emitted raw control characters or non-ASCII text (preventing header splitting/injection).
StringBuilder escaped = new StringBuilder(json.Length);
foreach (char c in json)
{
if (c < 0x20 || c >= 0x7f)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
escaped.Append("\\u").Append(((int)c).ToString("x4", CultureInfo.InvariantCulture));
else
escaped.Append(c);
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
}

return escaped.ToString();
}

/// <summary>
/// Encode string in base64 format.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -349,12 +349,12 @@ namespace {{packageName}}.{{apiPackage}}
{{/queryParams}}
{{#headerParams}}
{{#required}}
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // header parameter
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.{{#headerIsJsonMimeType}}ParameterToJsonString{{/headerIsJsonMimeType}}{{^headerIsJsonMimeType}}ParameterToString{{/headerIsJsonMimeType}}({{paramName}})); // header parameter
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
{{/required}}
{{^required}}
if ({{paramName}} != null)
{
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // header parameter
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.{{#headerIsJsonMimeType}}ParameterToJsonString{{/headerIsJsonMimeType}}{{^headerIsJsonMimeType}}ParameterToString{{/headerIsJsonMimeType}}({{paramName}})); // header parameter
}
{{/required}}
{{/headerParams}}
Expand Down Expand Up @@ -639,12 +639,12 @@ namespace {{packageName}}.{{apiPackage}}
{{/constantParams}}
{{#headerParams}}
{{#required}}
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // header parameter
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.{{#headerIsJsonMimeType}}ParameterToJsonString{{/headerIsJsonMimeType}}{{^headerIsJsonMimeType}}ParameterToString{{/headerIsJsonMimeType}}({{paramName}})); // header parameter
{{/required}}
{{^required}}
if ({{paramName}} != null)
{
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // header parameter
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.{{#headerIsJsonMimeType}}ParameterToJsonString{{/headerIsJsonMimeType}}{{^headerIsJsonMimeType}}ParameterToString{{/headerIsJsonMimeType}}({{paramName}})); // header parameter
}
{{/required}}
{{/headerParams}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -578,11 +578,11 @@ namespace {{packageName}}.{{apiPackage}}
// Set client side default value of Header Param "{{baseName}}".
if (ClientUtils.IsContentHeader("{{baseName}}"))
{
httpRequestMessageLocalVar.Content?.Headers.Add("{{baseName}}", ClientUtils.ParameterToString({{paramName}}));
httpRequestMessageLocalVar.Content?.Headers.Add("{{baseName}}", {{#headerIsJsonMimeType}}JsonSerializer.Serialize({{paramName}}, _jsonSerializerOptions){{/headerIsJsonMimeType}}{{^headerIsJsonMimeType}}ClientUtils.ParameterToString({{paramName}}){{/headerIsJsonMimeType}});
}
else
{
httpRequestMessageLocalVar.Headers.Add("{{baseName}}", ClientUtils.ParameterToString({{paramName}}));
httpRequestMessageLocalVar.Headers.Add("{{baseName}}", {{#headerIsJsonMimeType}}JsonSerializer.Serialize({{paramName}}, _jsonSerializerOptions){{/headerIsJsonMimeType}}{{^headerIsJsonMimeType}}ClientUtils.ParameterToString({{paramName}}){{/headerIsJsonMimeType}});
}

{{/required}}
Expand All @@ -592,11 +592,11 @@ namespace {{packageName}}.{{apiPackage}}
// Set client side default value of Header Param "{{baseName}}".
if (ClientUtils.IsContentHeader("{{baseName}}"))
{
httpRequestMessageLocalVar.Content?.Headers.Add("{{baseName}}", ClientUtils.ParameterToString({{paramName}}.Value));
httpRequestMessageLocalVar.Content?.Headers.Add("{{baseName}}", {{#headerIsJsonMimeType}}JsonSerializer.Serialize({{paramName}}.Value, _jsonSerializerOptions){{/headerIsJsonMimeType}}{{^headerIsJsonMimeType}}ClientUtils.ParameterToString({{paramName}}.Value){{/headerIsJsonMimeType}});
}
else
{
httpRequestMessageLocalVar.Headers.Add("{{baseName}}", ClientUtils.ParameterToString({{paramName}}.Value));
httpRequestMessageLocalVar.Headers.Add("{{baseName}}", {{#headerIsJsonMimeType}}JsonSerializer.Serialize({{paramName}}.Value, _jsonSerializerOptions){{/headerIsJsonMimeType}}{{^headerIsJsonMimeType}}ClientUtils.ParameterToString({{paramName}}.Value){{/headerIsJsonMimeType}});
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -430,12 +430,12 @@ namespace {{packageName}}.{{apiPackage}}
{{/queryParams}}
{{#headerParams}}
{{#required}}
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // header parameter
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.{{#headerIsJsonMimeType}}ParameterToJsonString{{/headerIsJsonMimeType}}{{^headerIsJsonMimeType}}ParameterToString{{/headerIsJsonMimeType}}({{paramName}})); // header parameter
{{/required}}
{{^required}}
if ({{paramName}} != null)
{
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // header parameter
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.{{#headerIsJsonMimeType}}ParameterToJsonString{{/headerIsJsonMimeType}}{{^headerIsJsonMimeType}}ParameterToString{{/headerIsJsonMimeType}}({{paramName}})); // header parameter
}
{{/required}}
{{/headerParams}}
Expand Down Expand Up @@ -647,12 +647,12 @@ namespace {{packageName}}.{{apiPackage}}
{{/constantParams}}
{{#headerParams}}
{{#required}}
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // header parameter
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.{{#headerIsJsonMimeType}}ParameterToJsonString{{/headerIsJsonMimeType}}{{^headerIsJsonMimeType}}ParameterToString{{/headerIsJsonMimeType}}({{paramName}})); // header parameter
{{/required}}
{{^required}}
if ({{paramName}} != null)
{
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // header parameter
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.{{#headerIsJsonMimeType}}ParameterToJsonString{{/headerIsJsonMimeType}}{{^headerIsJsonMimeType}}ParameterToString{{/headerIsJsonMimeType}}({{paramName}})); // header parameter
}
{{/required}}
{{/headerParams}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -356,12 +356,12 @@ namespace {{packageName}}.{{apiPackage}}
{{/queryParams}}
{{#headerParams}}
{{#required}}
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // header parameter
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.{{#headerIsJsonMimeType}}ParameterToJsonString{{/headerIsJsonMimeType}}{{^headerIsJsonMimeType}}ParameterToString{{/headerIsJsonMimeType}}({{paramName}})); // header parameter
{{/required}}
{{^required}}
if ({{paramName}} != null)
{
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // header parameter
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.{{#headerIsJsonMimeType}}ParameterToJsonString{{/headerIsJsonMimeType}}{{^headerIsJsonMimeType}}ParameterToString{{/headerIsJsonMimeType}}({{paramName}})); // header parameter
}
{{/required}}
{{/headerParams}}
Expand Down Expand Up @@ -567,12 +567,12 @@ namespace {{packageName}}.{{apiPackage}}
{{/queryParams}}
{{#headerParams}}
{{#required}}
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // header parameter
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.{{#headerIsJsonMimeType}}ParameterToJsonString{{/headerIsJsonMimeType}}{{^headerIsJsonMimeType}}ParameterToString{{/headerIsJsonMimeType}}({{paramName}})); // header parameter
{{/required}}
{{^required}}
if ({{paramName}} != null)
{
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.ParameterToString({{paramName}})); // header parameter
localVarRequestOptions.HeaderParameters.Add("{{baseName}}", {{packageName}}.Client.ClientUtils.{{#headerIsJsonMimeType}}ParameterToJsonString{{/headerIsJsonMimeType}}{{^headerIsJsonMimeType}}ParameterToString{{/headerIsJsonMimeType}}({{paramName}})); // header parameter
}
{{/required}}
{{/headerParams}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,69 @@ public void testHandleConstantParams() throws IOException {
"localVarRequestOptions.HeaderParameters.Add(\"X-CUSTOM_CONSTANT_HEADER\", Org.OpenAPITools.Client.ClientUtils.ParameterToString(\"CONSTANT_VALUE\"));");
}

@Test
public void testJsonContentHeaderUsesJsonSerialization() throws IOException {
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
output.deleteOnExit();
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_1/csharp/json-header-content.yaml");
final DefaultGenerator defaultGenerator = new DefaultGenerator();
final ClientOptInput clientOptInput = new ClientOptInput();
clientOptInput.openAPI(openAPI);
CSharpClientCodegen cSharpClientCodegen = new CSharpClientCodegen();
cSharpClientCodegen.setLibrary("restsharp");
cSharpClientCodegen.setOutputDir(output.getAbsolutePath());
clientOptInput.config(cSharpClientCodegen);
defaultGenerator.opts(clientOptInput);

Map<String, File> files = defaultGenerator.generate().stream()
.collect(Collectors.toMap(File::getPath, Function.identity()));

File apiFile = files
.get(Paths.get(output.getAbsolutePath(), "src", "Org.OpenAPITools", "Api", "DefaultApi.cs").toString());
assertNotNull(apiFile);
// JSON-content header is serialized as JSON, not via ParameterToString (which would emit the model's debug ToString()).
assertFileContains(apiFile.toPath(),
"localVarRequestOptions.HeaderParameters.Add(\"X-Json-Arg\", Org.OpenAPITools.Client.ClientUtils.ParameterToJsonString(xJsonArg)); // header parameter");
assertFileNotContains(apiFile.toPath(),
"localVarRequestOptions.HeaderParameters.Add(\"X-Json-Arg\", Org.OpenAPITools.Client.ClientUtils.ParameterToString(xJsonArg)); // header parameter");
// A vendor JSON media type (application/vnd.*+json) is also serialized as JSON.
assertFileContains(apiFile.toPath(),
"localVarRequestOptions.HeaderParameters.Add(\"X-Vendor-Json-Arg\", Org.OpenAPITools.Client.ClientUtils.ParameterToJsonString(xVendorJsonArg)); // header parameter");
// A regular (non-JSON) header keeps the existing behavior.
assertFileContains(apiFile.toPath(),
"localVarRequestOptions.HeaderParameters.Add(\"X-Plain-Arg\", Org.OpenAPITools.Client.ClientUtils.ParameterToString(xPlainArg)); // header parameter");
}

@Test
public void testJsonContentHeaderUsesJsonSerializationGenericHost() throws IOException {
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
output.deleteOnExit();
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_1/csharp/json-header-content.yaml");
final DefaultGenerator defaultGenerator = new DefaultGenerator();
final ClientOptInput clientOptInput = new ClientOptInput();
clientOptInput.openAPI(openAPI);
CSharpClientCodegen cSharpClientCodegen = new CSharpClientCodegen();
cSharpClientCodegen.setLibrary("generichost");
cSharpClientCodegen.setOutputDir(output.getAbsolutePath());
clientOptInput.config(cSharpClientCodegen);
defaultGenerator.opts(clientOptInput);

Map<String, File> files = defaultGenerator.generate().stream()
.collect(Collectors.toMap(File::getPath, Function.identity()));

File apiFile = files
.get(Paths.get(output.getAbsolutePath(), "src", "Org.OpenAPITools", "Api", "DefaultApi.cs").toString());
assertNotNull(apiFile);
// JSON-content header is serialized with System.Text.Json using the client's serializer options.
assertFileContains(apiFile.toPath(),
"JsonSerializer.Serialize(xJsonArg, _jsonSerializerOptions)");
// A vendor JSON media type (application/vnd.*+json) is also serialized as JSON.
assertFileContains(apiFile.toPath(),
"JsonSerializer.Serialize(xVendorJsonArg, _jsonSerializerOptions)");
assertFileContains(apiFile.toPath(),
"ClientUtils.ParameterToString(xPlainArg)");
}

@Test
public void testUserAgentIsNotUrlEncoded() throws IOException {
// both restsharp Configuration templates: the default one and the useIntForTimeout v7.9.0 fallback
Expand Down
Loading