diff --git a/src/main/java/io/vertx/openapi/impl/OpenAPIFormatValidator.java b/src/main/java/io/vertx/openapi/impl/OpenAPIFormatValidator.java index aa30951..be31716 100644 --- a/src/main/java/io/vertx/openapi/impl/OpenAPIFormatValidator.java +++ b/src/main/java/io/vertx/openapi/impl/OpenAPIFormatValidator.java @@ -4,6 +4,11 @@ public class OpenAPIFormatValidator implements JsonFormatValidator { + // The formats "float" and "double" define the precision of a number, not its representation. Whole numbers + // are valid, as long as they can be represented exactly with the related precision. + private static final long MAX_EXACT_FLOAT_INTEGER = 1L << 24; + private static final long MAX_EXACT_DOUBLE_INTEGER = 1L << 53; + @Override public String validateFormat(String instanceType, String format, Object instance) { if ("int32".equalsIgnoreCase(format) && !(instance instanceof Integer)) { @@ -15,16 +20,24 @@ public String validateFormat(String instanceType, String format, Object instance } if ("float".equalsIgnoreCase(format)) { - // Behind the scenes we use jackson, so even floats are converted into doubles for us. - // So now we will down cast the float back into a double, and check the usual isInfinite and isNan. - if (!(instance instanceof Double) || ((Float) ((Double) instance).floatValue()).isInfinite() + if (instance instanceof Integer || instance instanceof Long) { + if (!isExactlyRepresentable(((Number) instance).longValue(), MAX_EXACT_FLOAT_INTEGER)) { + return getMessage(format); + } + // Behind the scenes we use jackson, so even floats are converted into doubles for us. + // So now we will down cast the float back into a double, and check the usual isInfinite and isNan. + } else if (!(instance instanceof Double) || ((Float) ((Double) instance).floatValue()).isInfinite() || ((Float) ((Double) instance).floatValue()).isNaN()) { return getMessage(format); } } - if ("float".equalsIgnoreCase(format) || "double".equalsIgnoreCase(format)) { - if (!(instance instanceof Double) || ((Double) instance).isInfinite() || ((Double) instance).isNaN()) { + if ("double".equalsIgnoreCase(format)) { + if (instance instanceof Integer || instance instanceof Long) { + if (!isExactlyRepresentable(((Number) instance).longValue(), MAX_EXACT_DOUBLE_INTEGER)) { + return getMessage(format); + } + } else if (!(instance instanceof Double) || ((Double) instance).isInfinite() || ((Double) instance).isNaN()) { return getMessage(format); } } @@ -32,6 +45,10 @@ public String validateFormat(String instanceType, String format, Object instance return null; } + private static boolean isExactlyRepresentable(long value, long maxExactInteger) { + return value >= -maxExactInteger && value <= maxExactInteger; + } + private String getMessage(String format) { String type = "int32".equalsIgnoreCase(format) || "int64".equalsIgnoreCase(format) ? "Integer" : "Number"; return String.format("%s does not match the format \"%s\"", type, format); diff --git a/src/test/java/io/vertx/tests/validation/impl/RequestValidatorImplTest.java b/src/test/java/io/vertx/tests/validation/impl/RequestValidatorImplTest.java index ae36f00..bc5e32e 100644 --- a/src/test/java/io/vertx/tests/validation/impl/RequestValidatorImplTest.java +++ b/src/test/java/io/vertx/tests/validation/impl/RequestValidatorImplTest.java @@ -178,6 +178,18 @@ private static Stream getBadlyFormattedParameters() { Arguments.of("Double", numberSchema().toJson().put("format", "double"), "71" + Double.MAX_VALUE, "The value of path parameter Double is invalid. Reason: Number does not match the format \"double\""), Arguments.of("Float", numberSchema().toJson().put("format", "float"), "71" + Float.MAX_VALUE, + "The value of path parameter Float is invalid. Reason: Number does not match the format \"float\""), + Arguments.of("Double", numberSchema().toJson().put("format", "double"), 9007199254740993L, + "The value of path parameter Double is invalid. Reason: Number does not match the format \"double\""), + Arguments.of("Double", numberSchema().toJson().put("format", "double"), -9007199254740993L, + "The value of path parameter Double is invalid. Reason: Number does not match the format \"double\""), + Arguments.of("Double", numberSchema().toJson().put("format", "double"), Long.MAX_VALUE, + "The value of path parameter Double is invalid. Reason: Number does not match the format \"double\""), + Arguments.of("Float", numberSchema().toJson().put("format", "float"), 16777217, + "The value of path parameter Float is invalid. Reason: Number does not match the format \"float\""), + Arguments.of("Float", numberSchema().toJson().put("format", "float"), -16777217, + "The value of path parameter Float is invalid. Reason: Number does not match the format \"float\""), + Arguments.of("Float", numberSchema().toJson().put("format", "float"), Integer.MAX_VALUE, "The value of path parameter Float is invalid. Reason: Number does not match the format \"float\"")); } @@ -195,9 +207,17 @@ private static Stream getCorrectlyFormattedParameters() { Arguments.of("Double min double", numberSchema().toJson().put("format", "double"), Double.MIN_VALUE), Arguments.of("Double max float", numberSchema().toJson().put("format", "double"), Float.MAX_VALUE), Arguments.of("Double normal", numberSchema().toJson().put("format", "double"), 123.456), + Arguments.of("Double without decimal part", numberSchema().toJson().put("format", "double"), 10), + Arguments.of("Double max exact integer", numberSchema().toJson().put("format", "double"), + 9007199254740992L), + Arguments.of("Double min exact integer", numberSchema().toJson().put("format", "double"), + -9007199254740992L), Arguments.of("Float max float", numberSchema().toJson().put("format", "float"), Float.MAX_VALUE), Arguments.of("Float min float", numberSchema().toJson().put("format", "float"), Float.MIN_VALUE), - Arguments.of("Float normal", numberSchema().toJson().put("format", "float"), 123.456)); + Arguments.of("Float normal", numberSchema().toJson().put("format", "float"), 123.456), + Arguments.of("Float without decimal part", numberSchema().toJson().put("format", "float"), 10), + Arguments.of("Float max exact integer", numberSchema().toJson().put("format", "float"), 16777216), + Arguments.of("Float min exact integer", numberSchema().toJson().put("format", "float"), -16777216)); } @BeforeEach