3434
3535import java .io .File ;
3636import java .util .*;
37+ import java .util .regex .Matcher ;
3738import java .util .regex .Pattern ;
3839
3940import static org .openapitools .codegen .utils .CamelizeOption .LOWERCASE_FIRST_LETTER ;
@@ -177,17 +178,113 @@ public NimClientCodegen() {
177178 }
178179
179180
181+ @ Override
182+ public Map <String , ModelsMap > postProcessAllModels (Map <String , ModelsMap > allModels ) {
183+ allModels = super .postProcessAllModels (allModels );
184+
185+ // First pass: identify all models that have fields with custom JSON names
186+ Set <String > modelsWithCustomJson = new HashSet <>();
187+
188+ for (Map .Entry <String , ModelsMap > entry : allModels .entrySet ()) {
189+ ModelsMap modelsMap = entry .getValue ();
190+ for (ModelMap mo : modelsMap .getModels ()) {
191+ CodegenModel cm = mo .getModel ();
192+
193+ // Check if this model has fields with custom JSON names
194+ for (CodegenProperty var : cm .vars ) {
195+ if (var .vendorExtensions .containsKey ("x-json-name" )) {
196+ modelsWithCustomJson .add (cm .classname );
197+ break ;
198+ }
199+ }
200+ }
201+ }
202+
203+ // Second pass: cascade custom JSON handling to parent models and mark array fields
204+ // We need multiple passes to handle transitive dependencies
205+ boolean changed = true ;
206+ while (changed ) {
207+ changed = false ;
208+ for (Map .Entry <String , ModelsMap > entry : allModels .entrySet ()) {
209+ ModelsMap modelsMap = entry .getValue ();
210+ for (ModelMap mo : modelsMap .getModels ()) {
211+ CodegenModel cm = mo .getModel ();
212+
213+ // Check if any field's type needs custom JSON and mark array fields appropriately
214+ for (CodegenProperty var : cm .vars ) {
215+ String fieldType = var .complexType != null ? var .complexType : var .baseType ;
216+
217+ // Handle arrays - check if the inner type has custom JSON
218+ if (var .isArray && var .items != null ) {
219+ String innerType = var .items .complexType != null ? var .items .complexType : var .items .baseType ;
220+ if (innerType != null && modelsWithCustomJson .contains (innerType )) {
221+ // Mark this array field as containing types with custom JSON
222+ var .vendorExtensions .put ("x-is-array-with-custom-json" , "true" );
223+ var .vendorExtensions .put ("x-array-inner-type" , innerType );
224+ }
225+ fieldType = innerType ;
226+ }
227+
228+ // Cascade custom JSON to parent model if not already marked
229+ if (fieldType != null && modelsWithCustomJson .contains (fieldType )) {
230+ if (!cm .vendorExtensions .containsKey ("x-has-custom-json-names" )) {
231+ cm .vendorExtensions .put ("x-has-custom-json-names" , true );
232+ modelsWithCustomJson .add (cm .classname );
233+ changed = true ;
234+ }
235+ }
236+ }
237+ }
238+ }
239+ }
240+
241+ return allModels ;
242+ }
243+
244+ /**
245+ * Strips surrounding quotes from integer enum values.
246+ * The base OpenAPI Generator stores all enum values as quoted strings (e.g., "0", "1", "2")
247+ * regardless of the enum's actual type. For Nim integer enums, we need the raw numbers
248+ * without quotes so they serialize correctly: %(0) instead of %("0")
249+ */
250+ private void stripQuotesFromIntegerEnumValues (Map <String , Object > allowableValues ) {
251+ if (allowableValues == null || !allowableValues .containsKey ("enumVars" )) {
252+ return ;
253+ }
254+
255+ @ SuppressWarnings ("unchecked" )
256+ List <Map <String , Object >> enumVars = (List <Map <String , Object >>) allowableValues .get ("enumVars" );
257+ for (Map <String , Object > enumVar : enumVars ) {
258+ Object value = enumVar .get ("value" );
259+ if (value instanceof String ) {
260+ String strValue = (String ) value ;
261+ // Remove surrounding quotes if present
262+ if (strValue .startsWith ("\" " ) && strValue .endsWith ("\" " )) {
263+ enumVar .put ("value" , strValue .substring (1 , strValue .length () - 1 ));
264+ }
265+ }
266+ }
267+ }
268+
180269 @ Override
181270 public ModelsMap postProcessModels (ModelsMap objs ) {
182271 objs = postProcessModelsEnum (objs );
183272
184- // Mark top-level string enums for proper enum generation in template
185273 for (ModelMap mo : objs .getModels ()) {
186274 CodegenModel cm = mo .getModel ();
275+
187276 if (cm .isEnum && cm .allowableValues != null && cm .allowableValues .containsKey ("enumVars" )) {
188277 cm .vendorExtensions .put ("x-is-top-level-enum" , true );
278+
279+ // For integer enums, strip quotes from enum values
280+ if (cm .vendorExtensions .containsKey ("x-is-integer-enum" )) {
281+ stripQuotesFromIntegerEnumValues (cm .allowableValues );
282+ }
189283 }
190284
285+ // Check if any fields need custom JSON name mapping
286+ boolean hasCustomJsonNames = false ;
287+
191288 // Fix dataType fields that contain underscored type names
192289 // This handles cases like Table[string, Record_string__foo__value]
193290 // Also wrap optional fields in Option[T]
@@ -199,17 +296,47 @@ public ModelsMap postProcessModels(ModelsMap objs) {
199296 var .datatypeWithEnum = fixRecordTypeReferences (var .datatypeWithEnum );
200297 }
201298
299+ // Check if the field name was changed from the original (baseName)
300+ // This happens for fields like "_id" which are renamed to "id"
301+ // But we need to exclude cases where the name is just escaped with backticks
302+ // (e.g., "from" becomes "`from`" because it's a reserved word)
303+ if (var .baseName != null && !var .baseName .equals (var .name )) {
304+ // Check if this is just a reserved word escaping (name is `baseName`)
305+ String escapedName = "`" + var .baseName + "`" ;
306+ if (!var .name .equals (escapedName )) {
307+ // This is a real rename, not just escaping
308+ var .vendorExtensions .put ("x-json-name" , var .baseName );
309+ hasCustomJsonNames = true ;
310+ }
311+ }
312+
202313 // Wrap optional (non-required) or nullable fields in Option[T]
203- if ((!var .required || var .isNullable ) && !var .isReadOnly ) {
204- String baseType = var .datatypeWithEnum != null ? var .datatypeWithEnum : var .dataType ;
314+ // For non-enum fields only (enums are handled specially in the template)
315+ if ((!var .required || var .isNullable ) && !var .isReadOnly && !var .isEnum ) {
316+ String baseType = var .dataType ;
205317 if (baseType != null && !baseType .startsWith ("Option[" )) {
206318 var .dataType = "Option[" + baseType + "]" ;
207319 if (var .datatypeWithEnum != null ) {
208320 var .datatypeWithEnum = "Option[" + var .datatypeWithEnum + "]" ;
209321 }
210- var .vendorExtensions .put ("x-is-optional" , true );
211322 }
212323 }
324+
325+ // For enum fields, set x-is-optional if they are not required
326+ if (var .isEnum && (!var .required || var .isNullable )) {
327+ var .vendorExtensions .put ("x-is-optional" , true );
328+ }
329+
330+ // Always set x-is-optional based on the final dataType (for non-enum fields)
331+ // This ensures consistency between type declaration and JSON handling
332+ if (!var .isEnum && var .dataType != null && var .dataType .startsWith ("Option[" )) {
333+ var .vendorExtensions .put ("x-is-optional" , true );
334+ }
335+ }
336+
337+ // Mark the model as needing custom JSON deserialization if any fields have custom names
338+ if (hasCustomJsonNames ) {
339+ cm .vendorExtensions .put ("x-has-custom-json-names" , true );
213340 }
214341 }
215342
@@ -231,7 +358,7 @@ private String fixRecordTypeReferences(String typeString) {
231358
232359 // Match Record_ followed by any characters until end or comma/bracket
233360 Pattern pattern = Pattern .compile ("Record_[a-z_]+" );
234- java . util . regex . Matcher matcher = pattern .matcher (result );
361+ Matcher matcher = pattern .matcher (result );
235362
236363 StringBuffer sb = new StringBuffer ();
237364 while (matcher .find ()) {
@@ -331,6 +458,14 @@ public CodegenModel fromModel(String name, Schema schema) {
331458 name = normalizeSchemaName (name );
332459 CodegenModel mdl = super .fromModel (name , schema );
333460
461+ // Detect integer enums - check both the schema type and the dataType
462+ if (mdl .isEnum ) {
463+ String schemaType = schema != null ? schema .getType () : null ;
464+ if ("integer" .equals (schemaType ) || "int" .equals (mdl .dataType ) || "int64" .equals (mdl .dataType )) {
465+ mdl .vendorExtensions .put ("x-is-integer-enum" , true );
466+ }
467+ }
468+
334469 // Handle oneOf/anyOf schemas to use Nim object variants
335470 if (mdl .getComposedSchemas () != null ) {
336471 if (mdl .getComposedSchemas ().getOneOf () != null && !mdl .getComposedSchemas ().getOneOf ().isEmpty ()) {
@@ -364,12 +499,35 @@ private void processComposedSchemaVariants(CodegenModel mdl, List<CodegenPropert
364499 // Create a clone to avoid modifying the original
365500 CodegenProperty newVariant = variant .clone ();
366501
367- // Sanitize dataType to remove trailing underscores (Nim doesn't allow them)
502+ // Sanitize baseName to remove underscores and properly format for Nim
503+ if (newVariant .baseName != null ) {
504+ // Remove trailing underscores and convert to proper format
505+ String sanitizedBase = newVariant .baseName .replaceAll ("_+$" , "" ); // Remove trailing underscores
506+ if (sanitizedBase .length () > 0 && Character .isUpperCase (sanitizedBase .charAt (0 ))) {
507+ newVariant .baseName = toModelName (sanitizedBase );
508+ } else {
509+ newVariant .baseName = sanitizeNimIdentifier (sanitizedBase );
510+ }
511+ }
512+
513+ // Sanitize dataType to remove underscores and properly format for Nim
514+ // For model types (not primitives), use toModelName to get the proper type name
368515 if (newVariant .dataType != null ) {
369- newVariant .dataType = sanitizeNimIdentifier (newVariant .dataType );
516+ // Check if this is a model type (starts with uppercase) vs primitive
517+ if (newVariant .dataType .length () > 0 && Character .isUpperCase (newVariant .dataType .charAt (0 ))) {
518+ // This is likely a model type, use toModelName to properly format it
519+ newVariant .dataType = toModelName (newVariant .dataType );
520+ } else {
521+ // Primitive type, just sanitize
522+ newVariant .dataType = sanitizeNimIdentifier (newVariant .dataType );
523+ }
370524 }
371525 if (newVariant .datatypeWithEnum != null ) {
372- newVariant .datatypeWithEnum = sanitizeNimIdentifier (newVariant .datatypeWithEnum );
526+ if (newVariant .datatypeWithEnum .length () > 0 && Character .isUpperCase (newVariant .datatypeWithEnum .charAt (0 ))) {
527+ newVariant .datatypeWithEnum = toModelName (newVariant .datatypeWithEnum );
528+ } else {
529+ newVariant .datatypeWithEnum = sanitizeNimIdentifier (newVariant .datatypeWithEnum );
530+ }
373531 }
374532
375533 // Set variant name based on schema reference or type
0 commit comments