Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -61,6 +61,8 @@
import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime;
Expand All @@ -87,6 +89,10 @@ public abstract class AbstractJavaCodegen extends DefaultCodegen implements Code
private final Logger LOGGER = LoggerFactory.getLogger(AbstractJavaCodegen.class);
private static final String ARTIFACT_VERSION_DEFAULT_VALUE = "1.0.0";
private static final ZoneId UTC = ZoneId.of("UTC");
private static final BigInteger INTEGER_MIN_VALUE = BigInteger.valueOf(Integer.MIN_VALUE);
private static final BigInteger INTEGER_MAX_VALUE = BigInteger.valueOf(Integer.MAX_VALUE);
private static final BigInteger LONG_MIN_VALUE = BigInteger.valueOf(Long.MIN_VALUE);
private static final BigInteger LONG_MAX_VALUE = BigInteger.valueOf(Long.MAX_VALUE);

public static final String DEFAULT_LIBRARY = "<default>";
public static final String DATE_LIBRARY = "dateLibrary";
Expand Down Expand Up @@ -304,8 +310,10 @@ public AbstractJavaCodegen() {
typeMapping.put("date", "Date");
typeMapping.put("file", "File");
typeMapping.put("AnyType", "Object");
typeMapping.put("BigInteger", "BigInteger");
Comment thread
tisis2 marked this conversation as resolved.

importMapping.put("BigDecimal", "java.math.BigDecimal");
importMapping.put("BigInteger", "java.math.BigInteger");
importMapping.put("UUID", "java.util.UUID");
importMapping.put("URI", "java.net.URI");
importMapping.put("File", "java.io.File");
Expand Down Expand Up @@ -1909,6 +1917,21 @@ public String toExampleValue(Schema p) {

@Override
public String getSchemaType(Schema p) {
if (ModelUtils.isIntegerSchema(p)) {
// legacy, non-standard `uint32`/`uint64` integer formats: since Java has no native
// unsigned integer types, widen them to a type that can hold the full unsigned range
if ("uint32".equals(p.getFormat())) {
return typeMapping.get("long");
} else if ("uint64".equals(p.getFormat())) {
Comment thread
tisis2 marked this conversation as resolved.
return typeMapping.get("BigInteger");
} else if (StringUtils.isEmpty(p.getFormat()) && hasIntegerBounds(p)) {
// no format given: infer the smallest type (Integer/Long/BigInteger) that fits minimum/maximum,
// the same way the rust-axum generator picks its integer types
return bestFittingIntegerType(integerBound(p.getMinimum()), Boolean.TRUE.equals(p.getExclusiveMinimum()),
integerBound(p.getMaximum()), Boolean.TRUE.equals(p.getExclusiveMaximum()));
}
}

String openAPIType = super.getSchemaType(p);

// don't apply renaming on types from the typeMapping
Expand All @@ -1922,6 +1945,74 @@ public String getSchemaType(Schema p) {
return toModelName(openAPIType);
}

private boolean hasIntegerBounds(Schema p) {
return p.getMinimum() != null || p.getMaximum() != null;
}

private BigInteger integerBound(BigDecimal bound) {
return bound == null ? null : bound.toBigInteger();
Comment thread
tisis2 marked this conversation as resolved.
Outdated
}

/**
* Determine the smallest Java integer type (Integer, Long or BigInteger) that can represent every
* value in the given [minimum, maximum] range. Missing bounds are treated as unbounded on that side.
*/
private String bestFittingIntegerType(BigInteger minimum, boolean exclusiveMinimum,
BigInteger maximum, boolean exclusiveMaximum) {
if (exclusiveMinimum && minimum != null) {
minimum = minimum.add(BigInteger.ONE);
}
if (exclusiveMaximum && maximum != null) {
maximum = maximum.subtract(BigInteger.ONE);
}

if ((minimum == null || minimum.compareTo(INTEGER_MIN_VALUE) >= 0)
&& (maximum == null || maximum.compareTo(INTEGER_MAX_VALUE) <= 0)) {
return typeMapping.get("integer");
} else if ((minimum == null || minimum.compareTo(LONG_MIN_VALUE) >= 0)
&& (maximum == null || maximum.compareTo(LONG_MAX_VALUE) <= 0)) {
return typeMapping.get("long");
}
return typeMapping.get("BigInteger");
Comment on lines +1981 to +1988

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.

P2: For a one-sided integer range, the missing side is unbounded, so Integer or Long cannot represent every allowed value. Return BigInteger whenever either bound is absent, or otherwise evaluate both finite bounds.

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/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java, line 1980:

<comment>For a one-sided integer range, the missing side is unbounded, so `Integer` or `Long` cannot represent every allowed value. Return `BigInteger` whenever either bound is absent, or otherwise evaluate both finite bounds.</comment>

<file context>
@@ -1922,6 +1959,92 @@ public String getSchemaType(Schema p) {
+            maximum = maximum.subtract(BigDecimal.ONE);
+        }
+
+        if (Optional.ofNullable(minimum).map(this::fitsInInt).orElse(true)
+                && Optional.ofNullable(maximum).map(this::fitsInInt).orElse(true)) {
+            return typeMapping.get("integer");
</file context>
Suggested change
if (Optional.ofNullable(minimum).map(this::fitsInInt).orElse(true)
&& Optional.ofNullable(maximum).map(this::fitsInInt).orElse(true)) {
return typeMapping.get("integer");
} else if (Optional.ofNullable(minimum).map(this::fitsInLong).orElse(true)
&& Optional.ofNullable(maximum).map(this::fitsInLong).orElse(true)) {
return typeMapping.get("long");
}
return typeMapping.get("BigInteger");
if (minimum == null || maximum == null) {
return typeMapping.get("BigInteger");
} else if (fitsInInt(minimum) && fitsInInt(maximum)) {
return typeMapping.get("integer");
} else if (fitsInLong(minimum) && fitsInLong(maximum)) {
return typeMapping.get("long");
}
return typeMapping.get("BigInteger");

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

wouldn't that mean that every integer without a range that was previously generated as Integer, now would be generated as BigInteger and breaking the generated API usage?

}
Comment thread
tisis2 marked this conversation as resolved.
Outdated

@Override
protected void updatePropertyForInteger(CodegenProperty property, Schema p) {
// legacy, non-standard `uint32`/`uint64` integer formats (see getSchemaType above)
if ("uint32".equals(p.getFormat())) {
property.isNumeric = Boolean.TRUE;
property.isLong = Boolean.TRUE;
return;
} else if ("uint64".equals(p.getFormat())) {
property.isNumeric = Boolean.TRUE;
return;
} else if (StringUtils.isEmpty(p.getFormat()) && hasIntegerBounds(p)) {
property.isNumeric = Boolean.TRUE;
String inferredType = bestFittingIntegerType(integerBound(p.getMinimum()), Boolean.TRUE.equals(p.getExclusiveMinimum()),
integerBound(p.getMaximum()), Boolean.TRUE.equals(p.getExclusiveMaximum()));
if (typeMapping.get("long").equals(inferredType)) {
property.isLong = Boolean.TRUE;
} else if (!typeMapping.get("BigInteger").equals(inferredType)) {
property.isInteger = Boolean.TRUE;
}
return;
}
super.updatePropertyForInteger(property, p);
}

@Override
public void postProcessParameter(CodegenParameter parameter) {
// keep isLong/isInteger in sync with the widened dataType from uint32/uint64 formats and
// range-inferred Long/BigInteger types (see getSchemaType/updatePropertyForInteger above)
if (typeMapping.get("long").equals(parameter.dataType)) {
parameter.isInteger = false;
parameter.isLong = true;
} else if (typeMapping.get("BigInteger").equals(parameter.dataType)) {
parameter.isInteger = false;
parameter.isLong = false;
}
}

@Override
public String toOperationId(String operationId) {
// throw exception if method name is empty
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@

import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
Expand Down Expand Up @@ -149,6 +150,55 @@ Iterator<Library> librariesNotSupportingJackson() {
}


@Test
public void testUint32AndUint64Formats() {
final JavaClientCodegen codegen = new JavaClientCodegen();

CodegenProperty uint32Property = codegen.fromProperty("uint32Value", new IntegerSchema().format("uint32"));
Assertions.assertEquals(uint32Property.dataType, "Long");
Assertions.assertEquals(uint32Property.baseType, "Long");
Assertions.assertTrue(uint32Property.isLong);
Assertions.assertFalse(uint32Property.isInteger);

CodegenProperty uint64Property = codegen.fromProperty("uint64Value", new IntegerSchema().format("uint64"));
Assertions.assertEquals(uint64Property.dataType, "BigInteger");
Assertions.assertEquals(uint64Property.baseType, "BigInteger");
Assertions.assertFalse(uint64Property.isLong);
Assertions.assertFalse(uint64Property.isInteger);
}

@Test
public void testIntegerTypeInferredFromRangeWhenFormatIsMissing() {
final JavaClientCodegen codegen = new JavaClientCodegen();

// small range with no format: stays the default Integer
CodegenProperty smallRange = codegen.fromProperty("smallRange",
new IntegerSchema().minimum(BigDecimal.ZERO).maximum(BigDecimal.valueOf(255)));
Assertions.assertEquals(smallRange.dataType, "Integer");
Assertions.assertTrue(smallRange.isInteger);
Assertions.assertFalse(smallRange.isLong);

// range exceeding Integer bounds with no format: widen to Long
CodegenProperty exceedsInteger = codegen.fromProperty("exceedsInteger",
new IntegerSchema().maximum(BigDecimal.valueOf(Integer.MAX_VALUE).add(BigDecimal.ONE)));
Assertions.assertEquals(exceedsInteger.dataType, "Long");
Assertions.assertTrue(exceedsInteger.isLong);
Assertions.assertFalse(exceedsInteger.isInteger);

// range exceeding Long bounds with no format: widen to BigInteger
CodegenProperty exceedsLong = codegen.fromProperty("exceedsLong",
new IntegerSchema().maximum(BigDecimal.valueOf(Long.MAX_VALUE).add(BigDecimal.ONE)));
Assertions.assertEquals(exceedsLong.dataType, "BigInteger");
Assertions.assertFalse(exceedsLong.isLong);
Assertions.assertFalse(exceedsLong.isInteger);

// exclusiveMaximum pushes the effective bound just over the Integer limit
CodegenProperty exclusiveMax = codegen.fromProperty("exclusiveMax",
new IntegerSchema().maximum(BigDecimal.valueOf(Integer.MAX_VALUE).add(BigDecimal.valueOf(2))).exclusiveMaximum(true));
Assertions.assertEquals(exclusiveMax.dataType, "Long");
Assertions.assertTrue(exclusiveMax.isLong);
}

@Test
public void arraysInRequestBody() {
OpenAPI openAPI = TestUtils.createOpenAPI();
Expand Down