Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -1560,6 +1560,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", ModelUtils.isStringSchema(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,37 @@

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;

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 enum omits type: string but contains string values, isStringEnum can be false and the generator emits j = available instead of j = "available". Determine stringness from the enum values as a fallback, or preserve the per-enum string flag for untyped enums.

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 enum omits `type: string` but contains string values, `isStringEnum` can be false and the generator emits `j = available` instead of `j = "available"`. Determine stringness from the enum values as a fallback, or preserve the per-enum string flag for untyped enums.</comment>

<file context>
@@ -29,15 +28,15 @@ inline void to_json(nlohmann::json& j, const {{vendorExtensions.modelClassName}}
     {
         {{#allowableValues}}{{#enumVars}}
-        case {{vendorExtensions.modelClassName}}::{{name}}: j = {{#isString}}"{{{value}}}"{{/isString}}{{^isString}}{{value}}{{/isString}}; break;
+        case {{vendorExtensions.modelClassName}}::{{name}}: j = {{#vendorExtensions.isStringEnum}}"{{{value}}}"{{/vendorExtensions.isStringEnum}}{{^vendorExtensions.isStringEnum}}{{value}}{{/vendorExtensions.isStringEnum}}; break;
         {{/enumVars}}{{/allowableValues}}
     }
</file context>

{{/enumVars}}{{/allowableValues}}
}
}

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 @@ -252,5 +283,6 @@ private:

} // namespace {{vendorExtensions.modelNamespace}}

{{/isEnum}}
{{/model}}
{{/models}}
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 @@ -35,13 +35,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 +177,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 +225,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 +248,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 +278,20 @@ 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,

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.

P3: The new assertion only verifies the internal isStringEnum vendor flag on the fromModel result, not the generated serialization output, and the test schema is always string-backed. Because the template switch now keys quoting off this single model-level flag, the {{^vendorExtensions.isStringEnum}} branch for integer-backed top-level enums has no coverage, so a regression in the rendered to_json/from_json output would go undetected. Render/check the generated enum header, or add a case for an integer-backed top-level enum.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/test/java/org/openapitools/codegen/cpphttplibserver/CppHttplibServerCodegenModelTest.java, line 284:

<comment>The new assertion only verifies the internal `isStringEnum` vendor flag on the `fromModel` result, not the generated serialization output, and the test schema is always string-backed. Because the template switch now keys quoting off this single model-level flag, the `{{^vendorExtensions.isStringEnum}}` branch for integer-backed top-level enums has no coverage, so a regression in the rendered `to_json`/`from_json` output would go undetected. Render/check the generated enum header, or add a case for an integer-backed top-level enum.</comment>

<file context>
@@ -281,6 +281,8 @@ public void enumModelTest() {
         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,
+                "string-backed top-level enums must serialize their values as JSON strings");
         Assert.assertNotNull(model.allowableValues);
</file context>

"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 model with nullable property")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1006,6 +1006,10 @@
}
}
},
"TopLevelStatus": {
"type": "string",
"enum": ["active", "inactive", "pending"]
},
"SimpleObject": {
"type": "object",
"required": ["id", "name"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,5 @@ models/TestQueryParameters200Response.cpp
models/TestQueryParameters200Response.h
models/TestQueryParametersDeepObjectParameter.cpp
models/TestQueryParametersDeepObjectParameter.h
models/TopLevelStatus.cpp
models/TopLevelStatus.h
13 changes: 13 additions & 0 deletions samples/server/petstore/cpp-httplib-server/feature-test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,19 @@ std::string jsonString = json.dump();
// Deserialize from JSON
auto parsedModel = models::TestQueryParametersDeepObjectParameter::fromJson(nlohmann::json::parse(jsonString));
```
#### models::TopLevelStatus

```cpp
// Select an enum value
auto model = models::TopLevelStatus::ACTIVE;

// 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<models::TopLevelStatus>();
```

## Implementing API Handlers

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

/**
* This file is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
#include "TopLevelStatus.h"

Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* This file is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/

#pragma once
// System headers
#include <nlohmann/json.hpp>



namespace models {

enum class TopLevelStatus {

ACTIVE,

INACTIVE,

PENDING

};

inline void to_json(nlohmann::json& j, const TopLevelStatus& value)
{
switch (value)
{

case TopLevelStatus::ACTIVE: j = "active"; break;

case TopLevelStatus::INACTIVE: j = "inactive"; break;

case TopLevelStatus::PENDING: j = "pending"; break;

}
}

inline void from_json(const nlohmann::json& j, TopLevelStatus& value)
{

if (j == "active")
{
value = TopLevelStatus::ACTIVE;
return;
}

if (j == "inactive")
{
value = TopLevelStatus::INACTIVE;
return;
}

if (j == "pending")
{
value = TopLevelStatus::PENDING;
return;
}

throw nlohmann::json::type_error::create(302, "Invalid value for TopLevelStatus", &j);
}