Skip to content

[BUG][rust-axum] format: uint32/uint64 is generated as signed i32 in models and query parsing #23336

@LSX-s-Software

Description

@LSX-s-Software

Bug Report Checklist

  • Have you provided a full/minimal spec to reproduce the issue?
  • Have you validated the input using an OpenAPI validator?
  • Have you tested with the latest master to confirm the issue still exists?
  • Have you searched for related issues/PRs?
  • What's the actual output vs expected output?
  • [Optional] Sponsorship to speed up the bug fix or feature request (example)
Description

rust-axum does not honor OpenAPI integer formats uint32 and uint64.

When a schema property is declared as:

  • type: integer
  • format: uint32 or format: uint64

the generated Rust code still uses signed i32 for model fields and also uses <i32 as std::str::FromStr> in generated query parsing code.

This causes incorrect Rust types in generated server models and can also lead to type mismatches in generated parsing code.

I found a related issue, but it appears to be different in scope:

openapi-generator version

7.21.0

OpenAPI declaration file content or url
openapi: 3.0.3
info:
  title: Rust Axum Integer Type Mapping Test
  version: 1.0.0
paths:
  /integers:
    get:
      parameters:
        - name: legacy_uint32
          in: query
          required: true
          schema:
            type: integer
            format: uint32
        - name: legacy_uint64
          in: query
          required: true
          schema:
            type: integer
            format: uint64
Generation Details
openapi-generator generate -g rust-axum -I repro.yaml -o out
Steps to reproduce
  1. Save the spec above as repro.yaml
  2. Run the openapi-generator generate command above
  3. Inspect out/src/models.rs
  • Actual output
    pub count32: i32,
    pub count64: i32,
  • Expected output
    pub count32: u32,
    pub count64: u64,
Related issues/PRs
Suggest a fix
  • Override getSchemaType in RustAxumServerCodegen and handle integer schemas explicitly.
  • Treat legacy format: uint32 and format: uint64 as u32 and u64 for backward compatibility.
  • For standard OpenAPI integer formats:
    • type: integer, format: int32, minimum >= 0 -> u32
    • type: integer, format: int64, minimum >= 0 -> u64
  • If x-unsigned: true is present, map int32/int64 to unsigned Rust types as well.
  • If no format is provided, reuse the existing best-fit integer selection logic based on minimum/maximum.
  • Apply the same logic consistently for:
    • model properties
    • parameter types
    • array/map inner integer types

I also drafted a patch for the fix:

diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustAxumServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustAxumServerCodegen.java
index a6450d6f3f..d85b30c891 100644
--- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustAxumServerCodegen.java
+++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustAxumServerCodegen.java
@@ -44,6 +44,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.File;
+import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.nio.file.Path;
 import java.util.*;
@@ -1024,6 +1025,62 @@ public class RustAxumServerCodegen extends AbstractRustCodegen implements Codege
         return codegenParameter;
     }
 
+    private String getIntegerDataType(String format,
+                                      BigInteger minimum,
+                                      boolean exclusiveMinimum,
+                                      BigInteger maximum,
+                                      boolean exclusiveMaximum,
+                                      boolean explicitUnsigned) {
+        boolean unsigned = explicitUnsigned || canFitIntoUnsigned(minimum, exclusiveMinimum);
+
+        if (StringUtils.isEmpty(format)) {
+            return bestFittingIntegerType(
+                    minimum,
+                    exclusiveMinimum,
+                    maximum,
+                    exclusiveMaximum,
+                    true);
+        }
+
+        switch (format) {
+            case "uint32":
+                return "u32";
+            case "uint64":
+                return "u64";
+            case "int32":
+                return unsigned ? "u32" : "i32";
+            case "int64":
+                return unsigned ? "u64" : "i64";
+            default:
+                LOGGER.warn("The integer format '{}' is not recognized and will be ignored.", format);
+                return bestFittingIntegerType(
+                        minimum,
+                        exclusiveMinimum,
+                        maximum,
+                        exclusiveMaximum,
+                        true);
+        }
+    }
+
+    @Override
+    public String getSchemaType(Schema p) {
+        if (Objects.equals(p.getType(), "integer")) {
+            BigInteger minimum = Optional.ofNullable(p.getMinimum()).map(BigDecimal::toBigInteger).orElse(null);
+            BigInteger maximum = Optional.ofNullable(p.getMaximum()).map(BigDecimal::toBigInteger).orElse(null);
+            boolean explicitUnsigned = ModelUtils.isUnsignedIntegerSchema(p) || ModelUtils.isUnsignedLongSchema(p);
+
+            return getIntegerDataType(
+                    p.getFormat(),
+                    minimum,
+                    Optional.ofNullable(p.getExclusiveMinimum()).orElse(false),
+                    maximum,
+                    Optional.ofNullable(p.getExclusiveMaximum()).orElse(false),
+                    explicitUnsigned);
+        }
+
+        return super.getSchemaType(p);
+    }
+
     @Override
     public String toInstantiationType(final Schema p) {
         if (ModelUtils.isArraySchema(p)) {
@@ -1113,13 +1170,17 @@ public class RustAxumServerCodegen extends AbstractRustCodegen implements Codege
         }
 
         // Integer type fitting
-        if (Objects.equals(property.baseType, "integer")) {
+        if (Boolean.TRUE.equals(property.isInteger) || Boolean.TRUE.equals(property.isLong) || Objects.equals(property.baseType, "UnsignedInteger") || Objects.equals(property.baseType, "UnsignedLong")) {
             BigInteger minimum = Optional.ofNullable(property.getMinimum()).map(BigInteger::new).orElse(null);
             BigInteger maximum = Optional.ofNullable(property.getMaximum()).map(BigInteger::new).orElse(null);
-            property.dataType = bestFittingIntegerType(
-                    minimum, property.getExclusiveMinimum(),
-                    maximum, property.getExclusiveMaximum(),
-                    true);
+            boolean explicitUnsigned = Objects.equals(property.baseType, "UnsignedInteger") || Objects.equals(property.baseType, "UnsignedLong");
+            property.dataType = getIntegerDataType(
+                    property.dataFormat,
+                    minimum,
+                    property.getExclusiveMinimum(),
+                    maximum,
+                    property.getExclusiveMaximum(),
+                    explicitUnsigned);
         }
 
         property.name = underscore(property.name);

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions