Skip to content

Commit 99baae6

Browse files
authored
[cpp] Fix Nested Map & Additional Properties Support (#22639)
* progress fixing nested map support in cpp generators * [cpp] Fix Nested Map Support * additional properties support * fix potential np3 * cleanup javadoc * inline * explicit memory header
1 parent f2a6b6d commit 99baae6

29 files changed

Lines changed: 957 additions & 98 deletions

File tree

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

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,22 @@ public String toModelImport(String name) {
176176
return "#include \"" + folder + name + ".h\"";
177177
}
178178

179+
/**
180+
* Resolve a schema reference. If the schema has a $ref, return the referenced schema.
181+
* This is for nested maps.
182+
*/
183+
private Schema resolveSchema(Schema schema) {
184+
if (schema == null) {
185+
return null;
186+
}
187+
if (StringUtils.isNotEmpty(schema.get$ref())) {
188+
String ref = ModelUtils.getSimpleRef(schema.get$ref());
189+
Schema resolved = ModelUtils.getSchema(openAPI, ref);
190+
return resolved != null ? resolved : schema;
191+
}
192+
return schema;
193+
}
194+
179195
/**
180196
* Optional - type declaration. This is a String which is used by the templates to instantiate your
181197
* types. There is typically special handling for different property types
@@ -185,15 +201,22 @@ public String toModelImport(String name) {
185201
@Override
186202
@SuppressWarnings("rawtypes")
187203
public String getTypeDeclaration(Schema p) {
188-
String openAPIType = getSchemaType(p);
204+
// Resolve the schema to check for nested maps/arrays - refs that point to map schemas
205+
Schema resolved = resolveSchema(p);
189206

190-
if (ModelUtils.isArraySchema(p)) {
191-
Schema inner = ModelUtils.getSchemaItems(p);
207+
if (ModelUtils.isArraySchema(resolved)) {
208+
Schema inner = ModelUtils.getSchemaItems(resolved);
192209
return getSchemaType(p) + "<" + getTypeDeclaration(inner) + ">";
193-
} else if (ModelUtils.isMapSchema(p)) {
194-
Schema inner = ModelUtils.getAdditionalProperties(p);
195-
return getSchemaType(p) + "<QString, " + getTypeDeclaration(inner) + ">";
196-
} else if (ModelUtils.isBinarySchema(p)) {
210+
} else if (ModelUtils.isMapSchema(resolved)) {
211+
Schema inner = ModelUtils.getAdditionalProperties(resolved);
212+
// inner can be null if additionalProperties is a boolean or not present
213+
String innerType = inner != null ? getTypeDeclaration(inner) : PREFIX + "Object";
214+
return getSchemaType(p) + "<QString, " + innerType + ">";
215+
}
216+
217+
// For non-containers, use the original schema to preserve model names
218+
String openAPIType = getSchemaType(p);
219+
if (ModelUtils.isBinarySchema(p)) {
197220
return getSchemaType(p);
198221
} else if (ModelUtils.isFileSchema(p)) {
199222
return getSchemaType(p);
@@ -210,29 +233,32 @@ public String getTypeDeclaration(Schema p) {
210233
@Override
211234
@SuppressWarnings("rawtypes")
212235
public String toDefaultValue(Schema p) {
213-
if (ModelUtils.isBooleanSchema(p)) {
236+
Schema schema = resolveSchema(p);
237+
if (ModelUtils.isBooleanSchema(schema)) {
214238
return "false";
215-
} else if (ModelUtils.isDateSchema(p)) {
239+
} else if (ModelUtils.isDateSchema(schema)) {
216240
return "NULL";
217-
} else if (ModelUtils.isDateTimeSchema(p)) {
241+
} else if (ModelUtils.isDateTimeSchema(schema)) {
218242
return "NULL";
219-
} else if (ModelUtils.isNumberSchema(p)) {
220-
if (SchemaTypeUtil.FLOAT_FORMAT.equals(p.getFormat())) {
243+
} else if (ModelUtils.isNumberSchema(schema)) {
244+
if (SchemaTypeUtil.FLOAT_FORMAT.equals(schema.getFormat())) {
221245
return "0.0f";
222246
}
223247
return "0.0";
224-
} else if (ModelUtils.isIntegerSchema(p)) {
225-
if (SchemaTypeUtil.INTEGER64_FORMAT.equals(p.getFormat())) {
248+
} else if (ModelUtils.isIntegerSchema(schema)) {
249+
if (SchemaTypeUtil.INTEGER64_FORMAT.equals(schema.getFormat())) {
226250
return "0L";
227251
}
228252
return "0";
229-
} else if (ModelUtils.isMapSchema(p)) {
230-
Schema inner = ModelUtils.getAdditionalProperties(p);
231-
return "QMap<QString, " + getTypeDeclaration(inner) + ">()";
232-
} else if (ModelUtils.isArraySchema(p)) {
233-
Schema inner = ModelUtils.getSchemaItems(p);
253+
} else if (ModelUtils.isMapSchema(schema)) {
254+
Schema inner = ModelUtils.getAdditionalProperties(schema);
255+
// inner can be null if additionalProperties is a boolean or not present
256+
String innerType = inner != null ? getTypeDeclaration(inner) : PREFIX + "Object";
257+
return "QMap<QString, " + innerType + ">()";
258+
} else if (ModelUtils.isArraySchema(schema)) {
259+
Schema inner = ModelUtils.getSchemaItems(schema);
234260
return "QList<" + getTypeDeclaration(inner) + ">()";
235-
} else if (ModelUtils.isStringSchema(p)) {
261+
} else if (ModelUtils.isStringSchema(schema)) {
236262
return "QString(\"\")";
237263
} else if (!StringUtils.isEmpty(p.get$ref())) {
238264
return toModelName(ModelUtils.getSimpleRef(p.get$ref())) + "()";

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

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,15 @@ public CodegenModel fromModel(String name, Schema model) {
291291
}
292292
}
293293

294+
// Handle additionalProperties for models that have both properties and additionalProperties
295+
Schema addlProps = ModelUtils.getAdditionalProperties(model);
296+
if (addlProps != null && model.getProperties() != null && !model.getProperties().isEmpty()) {
297+
// This model has both defined properties AND additionalProperties
298+
codegenModel.additionalPropertiesType = getTypeDeclaration(addlProps);
299+
// Add import for web::json::value which is used to store additional properties
300+
codegenModel.imports.add("#include <cpprest/json.h>");
301+
}
302+
294303
return codegenModel;
295304
}
296305

@@ -362,6 +371,34 @@ public String toApiFilename(String name) {
362371
return toApiName(name);
363372
}
364373

374+
/**
375+
* Resolve a schema reference. If the schema has a $ref, return the referenced schema.
376+
* This is for nested maps.
377+
*/
378+
private Schema resolveSchema(Schema schema) {
379+
if (schema == null) {
380+
return null;
381+
}
382+
if (StringUtils.isNotEmpty(schema.get$ref())) {
383+
String ref = ModelUtils.getSimpleRef(schema.get$ref());
384+
Schema resolved = ModelUtils.getSchema(openAPI, ref);
385+
return resolved != null ? resolved : schema;
386+
}
387+
return schema;
388+
}
389+
390+
/**
391+
* Check if a schema is a pure map (has additionalProperties but no defined properties).
392+
* Schemas with both properties and additionalProperties should be treated as models, not maps.
393+
*/
394+
private boolean isPureMapSchema(Schema schema) {
395+
if (!ModelUtils.isMapSchema(schema)) {
396+
return false;
397+
}
398+
// If the schema has defined properties, it's not a pure map
399+
return schema.getProperties() == null || schema.getProperties().isEmpty();
400+
}
401+
365402
/**
366403
* Optional - type declaration. This is a String which is used by the
367404
* templates to instantiate your types. There is typically special handling
@@ -372,15 +409,23 @@ public String toApiFilename(String name) {
372409
*/
373410
@Override
374411
public String getTypeDeclaration(Schema p) {
375-
String openAPIType = getSchemaType(p);
412+
// Resolve the schema to check for nested maps/arrays - refs that point to map schemas
413+
Schema resolved = resolveSchema(p);
376414

377-
if (ModelUtils.isArraySchema(p)) {
378-
Schema inner = ModelUtils.getSchemaItems(p);
415+
if (ModelUtils.isArraySchema(resolved)) {
416+
Schema inner = ModelUtils.getSchemaItems(resolved);
379417
return getSchemaType(p) + "<" + getTypeDeclaration(inner) + ">";
380-
} else if (ModelUtils.isMapSchema(p)) {
381-
Schema inner = ModelUtils.getAdditionalProperties(p);
382-
return getSchemaType(p) + "<utility::string_t, " + getTypeDeclaration(inner) + ">";
383-
} else if (ModelUtils.isFileSchema(p) || ModelUtils.isBinarySchema(p)) {
418+
} else if (isPureMapSchema(resolved)) {
419+
// Only treat as map if it has additionalProperties but NO defined properties
420+
Schema inner = ModelUtils.getAdditionalProperties(resolved);
421+
// inner can be null if additionalProperties is a boolean or not present
422+
String innerType = inner != null ? getTypeDeclaration(inner) : "std::shared_ptr<Object>";
423+
return getSchemaType(p) + "<utility::string_t, " + innerType + ">";
424+
}
425+
426+
// For non-containers, use the original schema to preserve model names
427+
String openAPIType = getSchemaType(p);
428+
if (ModelUtils.isFileSchema(p) || ModelUtils.isBinarySchema(p)) {
384429
return "std::shared_ptr<" + openAPIType + ">";
385430
} else if (ModelUtils.isStringSchema(p)
386431
|| ModelUtils.isDateSchema(p) || ModelUtils.isDateTimeSchema(p)
@@ -414,8 +459,10 @@ public String toDefaultValue(Schema p) {
414459
return "0L";
415460
}
416461
return "0";
417-
} else if (ModelUtils.isMapSchema(p)) {
418-
String inner = getSchemaType(ModelUtils.getAdditionalProperties(p));
462+
} else if (isPureMapSchema(p)) {
463+
Schema innerSchema = ModelUtils.getAdditionalProperties(p);
464+
// innerSchema can be null if additionalProperties is a boolean or not present
465+
String inner = innerSchema != null ? getSchemaType(innerSchema) : "std::shared_ptr<Object>";
419466
return "std::map<utility::string_t, " + inner + ">()";
420467
} else if (ModelUtils.isArraySchema(p)) {
421468
String inner = getSchemaType(ModelUtils.getSchemaItems(p));

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

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -208,9 +208,23 @@ public String getTypeDeclaration(String str) {
208208
return str;
209209
}
210210

211+
/**
212+
* Resolve a schema reference. If the schema has a $ref, return the referenced schema.
213+
* This is for nested maps.
214+
*/
215+
private Schema resolveSchema(Schema schema) {
216+
if (schema == null) {
217+
return null;
218+
}
219+
if (StringUtils.isNotEmpty(schema.get$ref())) {
220+
String ref = ModelUtils.getSimpleRef(schema.get$ref());
221+
Schema resolved = ModelUtils.getSchema(openAPI, ref);
222+
return resolved != null ? resolved : schema;
223+
}
224+
return schema;
225+
}
226+
211227
private void makeTypeMappings() {
212-
// Types
213-
String cpp_array_type = "std::list";
214228
typeMapping = new HashMap<>();
215229

216230
typeMapping.put("string", "std::string");
@@ -219,7 +233,8 @@ private void makeTypeMappings() {
219233
typeMapping.put("long", "long");
220234
typeMapping.put("boolean", "bool");
221235
typeMapping.put("double", "double");
222-
typeMapping.put("array", cpp_array_type);
236+
typeMapping.put("array", "std::list");
237+
typeMapping.put("map", "std::map");
223238
typeMapping.put("number", "long");
224239
typeMapping.put("binary", "std::string");
225240
typeMapping.put("password", "std::string");
@@ -255,6 +270,19 @@ public String toInstantiationType(Schema p) {
255270

256271
@Override
257272
public String getTypeDeclaration(Schema p) {
273+
// Only resolve for nested maps - check if a $ref points to a map schema
274+
Schema resolved = resolveSchema(p);
275+
276+
// Handle nested maps: if a $ref resolves to a map schema, build the nested type
277+
if (ModelUtils.isMapSchema(resolved)) {
278+
Schema inner = ModelUtils.getAdditionalProperties(resolved);
279+
// inner can be null if additionalProperties is a boolean or not present
280+
String innerType = inner != null ? getTypeDeclaration(inner) : "std::string";
281+
return getSchemaType(p) + "<std::string, " + innerType + ">";
282+
}
283+
284+
// For everything else (including arrays), use the original behavior
285+
// The templates handle adding array item types themselves
258286
String openAPIType = getSchemaType(p);
259287
if (languageSpecificPrimitives.contains(openAPIType)) {
260288
return toModelName(openAPIType);
@@ -296,7 +324,7 @@ public String toModelImport(String name) {
296324
return "#include <string>";
297325
} else if (name.equals("std::list")) {
298326
return "#include <list>";
299-
} else if (name.equals("Map")) {
327+
} else if (name.equals("Map") || name.equals("std::map")) {
300328
return "#include <map>";
301329
}
302330
return "#include \"" + name + ".h\"";

modules/openapi-generator/src/main/resources/cpp-rest-sdk-client/model-header.mustache

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,23 @@ public:
261261
{{/isInherited}}
262262

263263
{{/vars}}
264+
{{#additionalPropertiesType}}
265+
266+
/// <summary>
267+
/// Get additional properties (properties not defined in the schema)
268+
/// </summary>
269+
std::map<utility::string_t, web::json::value> getAdditionalProperties() const;
270+
bool additionalPropertiesIsSet() const;
271+
void unsetAdditionalProperties();
272+
/// <summary>
273+
/// Set additional properties
274+
/// </summary>
275+
void setAdditionalProperties(const std::map<utility::string_t, web::json::value>& value);
276+
/// <summary>
277+
/// Add a single additional property
278+
/// </summary>
279+
void addAdditionalProperty(const utility::string_t& key, const web::json::value& value);
280+
{{/additionalPropertiesType}}
264281

265282
protected:
266283
{{#vars}}
@@ -285,6 +302,10 @@ protected:
285302
{{/isInherited}}
286303

287304
{{/vars}}
305+
{{#additionalPropertiesType}}
306+
std::map<utility::string_t, web::json::value> m_AdditionalProperties;
307+
bool m_AdditionalPropertiesIsSet;
308+
{{/additionalPropertiesType}}
288309
};
289310

290311
{{/isEnum}}

modules/openapi-generator/src/main/resources/cpp-rest-sdk-client/model-source.mustache

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,9 @@ void {{classname}}::setValue({{classname}}::e{{classname}} const value)
207207
{{/isNullable}}
208208
{{/isInherited}}
209209
{{/vars}}
210+
{{#additionalPropertiesType}}
211+
m_AdditionalPropertiesIsSet = false;
212+
{{/additionalPropertiesType}}
210213
}
211214

212215
{{classname}}::~{{classname}}()
@@ -262,6 +265,16 @@ web::json::value {{classname}}::toJson() const
262265
{{/isNullable}}
263266
{{/isInherited}}
264267
{{/vars}}
268+
{{#additionalPropertiesType}}
269+
// Serialize additional properties
270+
if(m_AdditionalPropertiesIsSet)
271+
{
272+
for(const auto& item : m_AdditionalProperties)
273+
{
274+
val[item.first] = item.second;
275+
}
276+
}
277+
{{/additionalPropertiesType}}
265278

266279
return val;
267280
}
@@ -295,6 +308,24 @@ bool {{classname}}::fromJson(const web::json::value& val)
295308
}
296309
{{/isInherited}}
297310
{{/vars}}
311+
{{#additionalPropertiesType}}
312+
// Capture additional properties (keys not defined in the schema)
313+
if(val.is_object())
314+
{
315+
for(const auto& item : val.as_object())
316+
{
317+
// Skip known properties
318+
{{#vars}}
319+
{{^isInherited}}
320+
if(item.first == utility::conversions::to_string_t(_XPLATSTR("{{baseName}}"))) continue;
321+
{{/isInherited}}
322+
{{/vars}}
323+
// This is an additional property
324+
m_AdditionalProperties[item.first] = item.second;
325+
m_AdditionalPropertiesIsSet = true;
326+
}
327+
}
328+
{{/additionalPropertiesType}}
298329
return ok;
299330
}
300331

@@ -515,6 +546,36 @@ void {{classname}}::unset{{name}}()
515546
{{/isNullable}}
516547
}
517548
{{/isInherited}}{{/vars}}
549+
{{#additionalPropertiesType}}
550+
551+
std::map<utility::string_t, web::json::value> {{classname}}::getAdditionalProperties() const
552+
{
553+
return m_AdditionalProperties;
554+
}
555+
556+
void {{classname}}::setAdditionalProperties(const std::map<utility::string_t, web::json::value>& value)
557+
{
558+
m_AdditionalProperties = value;
559+
m_AdditionalPropertiesIsSet = true;
560+
}
561+
562+
void {{classname}}::addAdditionalProperty(const utility::string_t& key, const web::json::value& value)
563+
{
564+
m_AdditionalProperties[key] = value;
565+
m_AdditionalPropertiesIsSet = true;
566+
}
567+
568+
bool {{classname}}::additionalPropertiesIsSet() const
569+
{
570+
return m_AdditionalPropertiesIsSet;
571+
}
572+
573+
void {{classname}}::unsetAdditionalProperties()
574+
{
575+
m_AdditionalProperties.clear();
576+
m_AdditionalPropertiesIsSet = false;
577+
}
578+
{{/additionalPropertiesType}}
518579
{{/isEnum}}
519580
{{/oneOf}}
520581
{{#modelNamespaceDeclarations}}

modules/openapi-generator/src/main/resources/cpp-rest-sdk-client/modelbase-header.mustache

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include <cpprest/json.h>
1818

1919
#include <map>
20+
#include <memory>
2021
#include <set>
2122
#include <vector>
2223

0 commit comments

Comments
 (0)