Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
f186960
fix(cpp-httplib-server): generate top-level enum models
stefanwitkowskiwmb Aug 27, 2026
c877a32
fix(cpp-httplib-server): preserve enum string values
stefanwitkowskiwmb Aug 28, 2026
67be047
fix(cpp-httplib-server): regenerate samples
stefanwitkowskiwmb Aug 28, 2026
ffbc0fa
fix(cpp-httplib-server): serialize top-level string enums
stefanwitkowskiwmb Aug 28, 2026
d80db91
Merge branch 'OpenAPITools:master' into bugfix/httplib-server_top_lev…
stefanwitkowskiwmb Aug 31, 2026
facbbb0
Merge branch 'OpenAPITools:master' into bugfix/httplib-server_top_lev…
stefanwitkowskiwmb Sep 3, 2026
24a7af8
model-header.mustache fix namespace closure
stefanwitkowskiwmb Sep 3, 2026
f368b5a
fix comments
stefanwitkowskiwmb Sep 3, 2026
753093e
regenerate samples
stefanwitkowskiwmb Sep 3, 2026
385e1c2
generate samples in codespace
stefanwitkowskiwmb Sep 3, 2026
d20ead3
test(cpp-httplib-server): cover string-valued enums with non-string type
stefanwitkowskiwmb Sep 4, 2026
a2a938d
test(cpp-httplib-server): assert rendered to_json/from_json for top-l…
stefanwitkowskiwmb Sep 4, 2026
501451d
add default branch to switch in to_json
stefanwitkowskiwmb Sep 4, 2026
be62cce
Merge branch 'OpenAPITools:master' into bugfix/httplib-server_top_lev…
stefanwitkowskiwmb Sep 4, 2026
47dd09b
align exceptions also for nested enum definitions
stefanwitkowskiwmb Sep 4, 2026
21cfca0
Merge branch 'bugfix/httplib-server_top_level_enum_schema' of https:/…
stefanwitkowskiwmb Sep 4, 2026
969fd96
Merge branch 'OpenAPITools:master' into bugfix/httplib-server_top_lev…
stefanwitkowskiwmb Sep 7, 2026
112e3e1
Revert nlohmann exceptions in cpp, as the corresponding methods are n…
stefanwitkowskiwmb Sep 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1543,6 +1543,17 @@ public String getTypeDeclaration(Schema p) {
return typeDecl;
}

// untyped enum schemas (no explicit `type: string`) are still string-backed if their values are strings
@SuppressWarnings("rawtypes")
private boolean isStringEnumSchema(Schema schema) {
if (ModelUtils.isStringSchema(schema)) {
return true;
}
List<?> enumValues = schema.getEnum();
return enumValues != null && !enumValues.isEmpty()
&& enumValues.stream().allMatch(value -> value instanceof String);
}

@Override
@SuppressWarnings({"rawtypes", "unchecked"})
public CodegenModel fromModel(String name, Schema schema) {
Expand All @@ -1560,6 +1571,9 @@ public CodegenModel fromModel(String name, Schema schema) {
if (model != null) {
model.name = toModelName(modelName);
model.classname = model.name;
if (model.isEnum) {
model.vendorExtensions.put("isStringEnum", isStringEnumSchema(schema));
}

if (ModelUtils.hasAllOf(schema) && this.openAPI != null) {
int refCount = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,18 @@ The server will start on `http://0.0.0.0:8080` with:
#### {{vendorExtensions.modelNamespace}}::{{vendorExtensions.modelClassName}}

```cpp
{{#isEnum}}
// Select an enum value
auto model = {{vendorExtensions.modelNamespace}}::{{vendorExtensions.modelClassName}}::{{#allowableValues}}{{#enumVars}}{{#-first}}{{name}}{{/-first}}{{/enumVars}}{{/allowableValues}};

// Serialize to JSON via the generated to_json free function
nlohmann::json json = model;
std::string jsonString = json.dump();

// Deserialize from JSON via the generated from_json free function
auto parsedModel = nlohmann::json::parse(jsonString).get<{{vendorExtensions.modelNamespace}}::{{vendorExtensions.modelClassName}}>();
{{/isEnum}}
{{^isEnum}}
// Create a model
auto model = {{vendorExtensions.modelNamespace}}::{{vendorExtensions.modelClassName}}();
{{#vars}}
Expand All @@ -109,6 +121,7 @@ std::string jsonString = json.dump();

// Deserialize from JSON
auto parsedModel = {{vendorExtensions.modelNamespace}}::{{vendorExtensions.modelClassName}}::fromJson(nlohmann::json::parse(jsonString));
{{/isEnum}}
```
{{/model}}
{{/models}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,38 @@

namespace {{vendorExtensions.modelNamespace}} {

{{#isEnum}}
enum class {{vendorExtensions.modelClassName}} {
{{#allowableValues}}{{#enumVars}}
{{name}}{{^-last}},{{/-last}}
{{/enumVars}}{{/allowableValues}}
};

inline void to_json(nlohmann::json& j, const {{vendorExtensions.modelClassName}}& value)
{
switch (value)
{
{{#allowableValues}}{{#enumVars}}
case {{vendorExtensions.modelClassName}}::{{name}}: j = {{#vendorExtensions.isStringEnum}}"{{{value}}}"{{/vendorExtensions.isStringEnum}}{{^vendorExtensions.isStringEnum}}{{value}}{{/vendorExtensions.isStringEnum}}; break;
Comment thread
stefanwitkowskiwmb marked this conversation as resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When a top-level string enum contains a quote, backslash, or control character, this raw interpolation generates an invalid or changed C++ literal in both to_json and from_json. Generate a C++-escaped string literal in the codegen metadata and emit that literal without adding another pair of quotes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/resources/cpp-httplib-server/model-header.mustache, line 31:

<comment>When a top-level string enum contains a quote, backslash, or control character, this raw interpolation generates an invalid or changed C++ literal in both `to_json` and `from_json`. Generate a C++-escaped string literal in the codegen metadata and emit that literal without adding another pair of quotes.</comment>

<file context>
@@ -16,6 +16,38 @@
+    switch (value)
+    {
+        {{#allowableValues}}{{#enumVars}}
+        case {{vendorExtensions.modelClassName}}::{{name}}: j = {{#vendorExtensions.isStringEnum}}"{{{value}}}"{{/vendorExtensions.isStringEnum}}{{^vendorExtensions.isStringEnum}}{{value}}{{/vendorExtensions.isStringEnum}}; break;
+        {{/enumVars}}{{/allowableValues}}
+        default: throw nlohmann::json::type_error::create(302, "Invalid value for {{vendorExtensions.modelClassName}}", &j);
</file context>

{{/enumVars}}{{/allowableValues}}
default: throw nlohmann::json::type_error::create(302, "Invalid value for {{vendorExtensions.modelClassName}}", &j);
}
}

inline void from_json(const nlohmann::json& j, {{vendorExtensions.modelClassName}}& value)
{
{{#allowableValues}}{{#enumVars}}
if (j == {{#vendorExtensions.isStringEnum}}"{{{value}}}"{{/vendorExtensions.isStringEnum}}{{^vendorExtensions.isStringEnum}}{{value}}{{/vendorExtensions.isStringEnum}})
{
value = {{vendorExtensions.modelClassName}}::{{name}};
return;
}
{{/enumVars}}{{/allowableValues}}
throw nlohmann::json::type_error::create(302, "Invalid value for {{vendorExtensions.modelClassName}}", &j);
}

{{/isEnum}}
{{^isEnum}}
{{#vendorExtensions.isOneOfSchema}}
// oneOf schema - type alias for std::variant
using {{vendorExtensions.modelClassName}} = std::variant<
Expand Down Expand Up @@ -250,6 +282,7 @@ private:
{{/vendorExtensions.isAnyOfSchema}}
{{/vendorExtensions.isOneOfSchema}}

{{/isEnum}}
} // namespace {{vendorExtensions.modelNamespace}}

{{/model}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@

{{#models}}
{{#model}}
{{#isEnum}}
#include "{{vendorExtensions.modelClassName}}.h"

{{/isEnum}}
{{^isEnum}}
{{^vendorExtensions.isOneOfSchema}}
{{^vendorExtensions.isAnyOfSchema}}
#include "{{vendorExtensions.modelClassName}}.h"
Expand Down Expand Up @@ -79,5 +84,6 @@ std::string {{classname}}::{{enumName}}ToString({{classname}}::{{enumName}} valu
{{^vendorExtensions.isOneOfSchema}}{{^vendorExtensions.isAnyOfSchema}}
} // namespace {{vendorExtensions.modelNamespace}}
{{/vendorExtensions.isAnyOfSchema}}{{/vendorExtensions.isOneOfSchema}}
{{/isEnum}}
{{/model}}
{{/models}}
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,15 @@
import org.openapitools.codegen.languages.CppHttplibServerCodegen;
import org.openapitools.codegen.model.ModelMap;
import org.openapitools.codegen.model.ModelsMap;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.media.*;
import org.testng.Assert;
import org.testng.annotations.Test;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
Expand All @@ -35,13 +40,17 @@ public class CppHttplibServerCodegenModelTest {
* Wraps a single model the way {@link org.openapitools.codegen.DefaultGenerator}
* does before calling {@code postProcessAllModels}, so tests can exercise the full
* enum vendor-extension pipeline (identifier + original-value derivation), not just
* the intermediate state produced by {@code fromModel}.
* the intermediate state produced by {@code fromModel}. This mirrors
* {@code DefaultGenerator}'s {@code processModels}, which runs {@code postProcessModels}
* (and, for C++, {@code postProcessModelsEnum} with it) before {@code postProcessAllModels}
* is ever invoked.
*/
private Map<String, ModelsMap> wrapForPostProcessAllModels(String name, CodegenModel model) {
private Map<String, ModelsMap> wrapForPostProcessAllModels(CppHttplibServerCodegen codegen, String name, CodegenModel model) {
final ModelMap modelMap = new ModelMap();
modelMap.setModel(model);
final ModelsMap modelsMap = new ModelsMap();
modelsMap.setModels(Collections.singletonList(modelMap));
codegen.postProcessModels(modelsMap);
final HashMap<String, ModelsMap> allModels = new HashMap<>();
allModels.put(name, modelsMap);
return allModels;
Expand Down Expand Up @@ -173,7 +182,7 @@ public void arrayOfEnumsDeclaresValidUpperCaseIdentifiersTest() {

final CodegenModel model = codegen.fromModel("ModelWithColorArray", schema);
final CodegenModel processedModel = codegen.postProcessAllModels(
wrapForPostProcessAllModels("ModelWithColorArray", model))
wrapForPostProcessAllModels(codegen, "ModelWithColorArray", model))
.get("ModelWithColorArray").getModels().get(0).getModel();

CodegenProperty arrayProp = processedModel.vars.get(0);
Expand Down Expand Up @@ -221,7 +230,7 @@ public void numericEnumPropertyTest() {
// postProcessAllModels, since that's the single place both the identifier and the
// original spec value are derived together (see enumSerializationUsesOriginalSpecValueTest).
final CodegenModel processedModel = codegen.postProcessAllModels(
wrapForPostProcessAllModels("UserStatusModel", model))
wrapForPostProcessAllModels(codegen, "UserStatusModel", model))
.get("UserStatusModel").getModels().get(0).getModel();
CodegenProperty statusProp = processedModel.vars.get(0);
Assert.assertTrue((boolean) statusProp.vendorExtensions.getOrDefault("isEnum", false));
Expand All @@ -244,7 +253,7 @@ public void enumSerializationUsesOriginalSpecValueTest() {

final CodegenModel model = codegen.fromModel("Pet", schema);
final CodegenModel processedModel = codegen.postProcessAllModels(
wrapForPostProcessAllModels("Pet", model))
wrapForPostProcessAllModels(codegen, "Pet", model))
.get("Pet").getModels().get(0).getModel();

CodegenProperty statusProp = processedModel.vars.get(0);
Expand Down Expand Up @@ -274,15 +283,150 @@ public void enumModelTest() {

final CodegenModel model = codegen.fromModel("Status", enumSchema);

// Note: The C++ httplib server generator may not process enum-only models
// in the same way as regular object models. The model might be null or empty.
if (model != null) {
Assert.assertEquals(model.name, "Status");
// Check if it's marked as an enum in vendor extensions
if (model.vendorExtensions.containsKey("x-is-enum")) {
Assert.assertEquals(model.vendorExtensions.get("x-is-enum"), true);
}
}
Assert.assertNotNull(model);
Assert.assertEquals(model.name, "Status");
Assert.assertTrue(model.isEnum, "top-level enum schemas must remain enum models");
Assert.assertEquals(model.vendorExtensions.get("isStringEnum"), true,
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
"string-backed top-level enums must serialize their values as JSON strings");
Assert.assertNotNull(model.allowableValues);
Assert.assertEquals(model.allowableValues.get("values"),
java.util.Arrays.asList("ACTIVE", "INACTIVE", "PENDING"));

final CodegenModel processedModel = codegen.postProcessAllModels(
wrapForPostProcessAllModels(codegen, "Status", model))
.get("Status").getModels().get(0).getModel();
Assert.assertNotNull(processedModel.vendorExtensions.get("modelClassName"));
Assert.assertEquals(((List<?>) processedModel.allowableValues.get("enumVars")).size(), 3);
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
}

@Test(description = "convert integer-backed top-level enum model")
public void integerBackedEnumModelTest() {
final CppHttplibServerCodegen codegen = new CppHttplibServerCodegen();
codegen.processOpts();

IntegerSchema enumSchema = new IntegerSchema();
enumSchema.setEnum(java.util.Arrays.asList(0, 1, 2));

final CodegenModel model = codegen.fromModel("NumericStatus", enumSchema);

Assert.assertNotNull(model);
Assert.assertTrue(model.isEnum, "top-level enum schemas must remain enum models");
// model-header.mustache's `{{^vendorExtensions.isStringEnum}}` branch renders these
// values unquoted, so the flag must be false for integer-backed top-level enums.
Assert.assertEquals(model.vendorExtensions.get("isStringEnum"), false,
"integer-backed top-level enums must serialize their values as raw JSON numbers");
}

@Test(description = "convert 3.1 nullable top-level enum model with string values")
public void nullableStringEnumModelTest() {
final CppHttplibServerCodegen codegen = new CppHttplibServerCodegen();
codegen.processOpts();

// OpenAPI 3.1 `type: [null, string]`. ModelUtils.getType() returns the *first* declared
// type, so ModelUtils.isStringSchema() is false here even though the enum is string-backed.
JsonSchema enumSchema = new JsonSchema();
enumSchema.addType("null");
enumSchema.addType("string");
enumSchema.setEnum(java.util.Arrays.asList("available", "pending", "sold"));

final CodegenModel model = codegen.fromModel("NullableStatus", enumSchema);

Assert.assertNotNull(model);
Assert.assertTrue(model.isEnum, "top-level enum schemas must remain enum models");
// Without the value-based fallback the template would emit `j = available;` instead of
// `j = "available";`, which does not compile.
Assert.assertEquals(model.vendorExtensions.get("isStringEnum"), true,
"string-valued top-level enums must serialize their values as JSON strings even when"
+ " the declared type is not plainly `string`");
}

@Test(description = "model-header.mustache must render quoted values for string-backed "
+ "top-level enums and bare values for integer-backed ones; the other enum tests "
+ "only assert the isStringEnum flag that feeds this template, not its output")
public void topLevelEnumHeadersRenderTypedJsonValuesTest() throws IOException {
final File output = Files.createTempDirectory("cpp-httplib-server-enums").toFile();
output.deleteOnExit();

StringSchema stringEnumSchema = new StringSchema();
stringEnumSchema.setEnum(java.util.Arrays.asList("active", "inactive", "pending"));
IntegerSchema integerEnumSchema = new IntegerSchema();
integerEnumSchema.setEnum(java.util.Arrays.asList(0, 1, 2));

final OpenAPI openAPI = TestUtils.createOpenAPI();
openAPI.getComponents().addSchemas("TopLevelStatus", stringEnumSchema);
openAPI.getComponents().addSchemas("TopLevelPriority", integerEnumSchema);

final CppHttplibServerCodegen codegen = new CppHttplibServerCodegen();
codegen.additionalProperties().put("modelNamespace", "models");
codegen.setOutputDir(output.getAbsolutePath());

final List<File> files = new DefaultGenerator()
.opts(new ClientOptInput().openAPI(openAPI).config(codegen))
.generate();
files.forEach(File::deleteOnExit);

// model-header.mustache guards the quoting with `{{#vendorExtensions.isStringEnum}}`, so a
// `type: string` enum must emit quoted values; bare ones would be undeclared identifiers.
final Path stringEnumHeader = output.toPath().resolve("models/TopLevelStatus.h");
TestUtils.assertFileContains(stringEnumHeader,
"case TopLevelStatus::ACTIVE: j = \"active\"; break;",
"case TopLevelStatus::INACTIVE: j = \"inactive\"; break;",
"case TopLevelStatus::PENDING: j = \"pending\"; break;",
"if (j == \"active\")");
TestUtils.assertFileNotContains(stringEnumHeader, "j = active;");
// An enumerator outside the declared set must fail loudly rather than leave `j` untouched,
// mirroring the throw that from_json already performs on an unrecognised JSON value.
TestUtils.assertFileContains(stringEnumHeader,
"default: throw nlohmann::json::type_error::create(302, "
+ "\"Invalid value for TopLevelStatus\", &j);");

// ...while the `{{^vendorExtensions.isStringEnum}}` branch must leave a `type: integer`
// enum's values bare, or they serialize as JSON strings instead of numbers.
final Path integerEnumHeader = output.toPath().resolve("models/TopLevelPriority.h");
TestUtils.assertFileContains(integerEnumHeader,
"case TopLevelPriority::_0: j = 0; break;",
"case TopLevelPriority::_1: j = 1; break;",
"case TopLevelPriority::_2: j = 2; break;",
"if (j == 0)");
TestUtils.assertFileNotContains(integerEnumHeader, "j = \"0\";");
TestUtils.assertFileContains(integerEnumHeader,
"default: throw nlohmann::json::type_error::create(302, "
+ "\"Invalid value for TopLevelPriority\", &j);");
}

@Test(description = "model-source.mustache's ToString and FromString for an enum property "
+ "are plain C++ conversion helpers, not nlohmann (de)serializers, so an unrecognised "
+ "value must not throw nlohmann::json::type_error: ToString falls back to an empty "
+ "string and FromString throws plain std::invalid_argument")
public void propertyEnumConversionRejectsUnknownValuesTest() throws IOException {
final File output = Files.createTempDirectory("cpp-httplib-server-prop-enums").toFile();
output.deleteOnExit();

StringSchema statusSchema = new StringSchema();
statusSchema.setEnum(java.util.Arrays.asList("available", "pending", "sold"));
ObjectSchema petSchema = new ObjectSchema();
petSchema.addProperty("status", statusSchema);

final OpenAPI openAPI = TestUtils.createOpenAPI();
openAPI.getComponents().addSchemas("Pet", petSchema);

final CppHttplibServerCodegen codegen = new CppHttplibServerCodegen();
codegen.additionalProperties().put("modelNamespace", "models");
codegen.setOutputDir(output.getAbsolutePath());

final List<File> files = new DefaultGenerator()
.opts(new ClientOptInput().openAPI(openAPI).config(codegen))
.generate();
files.forEach(File::deleteOnExit);

// Property enums (de)serialize through StatusEnumToString/StatusEnumFromString rather
// than the switch/if-chain in model-header.mustache's top-level to_json/from_json, and
// those helpers don't depend on nlohmann::json at all.
final Path source = output.toPath().resolve("models/Pet.cpp");
TestUtils.assertFileContains(source,
"default: return {};",
"throw std::invalid_argument(\"Invalid enum value\");");
TestUtils.assertFileNotContains(source, "nlohmann::json::type_error::create(302, \"Invalid value for Pet::StatusEnum\")");
}

@Test(description = "convert model with nullable property")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1006,6 +1006,14 @@
}
}
},
"TopLevelStatus": {
"type": "string",
"enum": ["active", "inactive", "pending"]
},
"TopLevelPriority": {
"type": "integer",
"enum": [0, 1, 2]
},
"SimpleObject": {
"type": "object",
"required": ["id", "name"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,7 @@ models/TestQueryParameters200Response.cpp
models/TestQueryParameters200Response.h
models/TestQueryParametersDeepObjectParameter.cpp
models/TestQueryParametersDeepObjectParameter.h
models/TopLevelPriority.cpp
models/TopLevelPriority.h
models/TopLevelStatus.cpp
models/TopLevelStatus.h
Loading