From 159f5bb074d5818c97ebcb1c56abea8aadae26ce Mon Sep 17 00:00:00 2001 From: Stefano Baghino Date: Thu, 18 Jun 2026 06:55:09 +0200 Subject: [PATCH] Build against networknt json-schema-validator 3.x networknt 3.x replaced its Jackson 2 node API with Jackson 3 and removed the getSchema/validate(JsonNode) overloads this library called, so consumers that put networknt 3.x on the classpath hit NoSuchMethodError when the first schema is compiled. Feed networknt JSON text via its InputFormat.JSON entry points, move the custom Format validators to Jackson 3 nodes, and pin the Jackson 3 BOM to the version networknt depends on. The public API is unchanged. One behaviour change: a value beyond Double.MAX_VALUE is now rejected as a type error ("number expected") rather than reaching the custom double-format validator. The value is still rejected. Refs atlassian/openapi-request-validator#21 --- .../oai/validator/schema/SchemaValidator.java | 25 ++++++++++++++++--- .../validator/schema/format/DoubleFormat.java | 4 +-- .../validator/schema/format/FloatFormat.java | 17 ++++++++++--- .../validator/schema/format/Int32Format.java | 2 +- .../validator/schema/format/Int64Format.java | 2 +- .../oai/validator/util/OpenApiLoader.java | 2 +- .../validator/schema/SchemaValidatorTest.java | 5 +++- .../v31analysis/NetworkntDirectProbeTest.java | 9 +++---- .../v31analysis/UnevaluatedDeepProbeTest.java | 6 ++--- pom.xml | 12 ++++++++- 10 files changed, 60 insertions(+), 24 deletions(-) diff --git a/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/schema/SchemaValidator.java b/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/schema/SchemaValidator.java index 30c55c50..08122b15 100644 --- a/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/schema/SchemaValidator.java +++ b/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/schema/SchemaValidator.java @@ -19,6 +19,7 @@ import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.LoadingCache; import com.networknt.schema.Error; +import com.networknt.schema.InputFormat; import com.networknt.schema.InvalidSchemaException; import com.networknt.schema.SchemaRegistry; import com.networknt.schema.SchemaRegistryConfig; @@ -41,6 +42,7 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.io.IOException; +import java.io.UncheckedIOException; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; @@ -142,7 +144,7 @@ public SchemaValidator(@Nonnull final OpenAPI api, key.forResponse, definitions ); - return schemaRegistry.getSchema(schemaObject); + return schemaRegistry.getSchema(writeAsString(schemaObject), InputFormat.JSON); }); } else { this.jsonSchemaCache = null; @@ -239,7 +241,8 @@ public ValidationReport validate(@Nonnull final JsonNodeSupplier supplier, try { final com.networknt.schema.Schema resolvedJsonSchema = resolveJsonSchema(schema, keyPrefix); - final List validationMessages = resolvedJsonSchema.validate(supplier.get()); + final List validationMessages = + resolvedJsonSchema.validate(writeAsString(supplier.get()), InputFormat.JSON); return messageConverter.toValidationReport(validationMessages, keyPrefix); } catch (final InvalidSchemaException e) { return ValidationReport.singleton( @@ -265,7 +268,7 @@ private com.networknt.schema.Schema resolveJsonSchema(final Schema schema, return jsonSchemaCache.get(jsonSchemaKey); } final JsonNode schemaObject = readAndTransformSchemaObject(schema, forRequest, forResponse, definitions); - return schemaRegistry.getSchema(schemaObject); + return schemaRegistry.getSchema(writeAsString(schemaObject), InputFormat.JSON); } catch (final Exception e) { // TODO: Need to handle this exception throw new RuntimeException("JsonSchema construction failed", e); @@ -294,6 +297,22 @@ private JsonNode readAndTransformSchemaObject(final Schema schema, return schemaObject; } + /** + * Serialize a (Jackson 2) JSON node to a JSON string. + *

+ * swagger-core / swagger-parser produce Jackson 2 nodes, whereas networknt 3.x consumes Jackson 3. + * Rather than leak Jackson 2 nodes into networknt (which removed the Jackson 2 node overloads), + * the schema and the instance being validated are handed to networknt as JSON text via its + * {@link InputFormat#JSON} entry points. + */ + private String writeAsString(final JsonNode node) { + try { + return (isOpenApi30 ? Json.mapper() : Json31.mapper()).writeValueAsString(node); + } catch (final IOException e) { + throw new UncheckedIOException("Failed to serialize JSON node", e); + } + } + private boolean additionalPropertiesValidationEnabled() { return !messages.isIgnored(ADDITIONAL_PROPERTIES_KEY); } diff --git a/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/schema/format/DoubleFormat.java b/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/schema/format/DoubleFormat.java index 6b38b88a..b94303f5 100644 --- a/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/schema/format/DoubleFormat.java +++ b/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/schema/format/DoubleFormat.java @@ -1,12 +1,12 @@ package com.atlassian.oai.validator.schema.format; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.NumericNode; import com.networknt.schema.ExecutionContext; import com.networknt.schema.SchemaContext; import com.networknt.schema.format.Format; import com.networknt.schema.utils.JsonType; import com.networknt.schema.utils.TypeFactory; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.node.NumericNode; import java.math.BigDecimal; diff --git a/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/schema/format/FloatFormat.java b/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/schema/format/FloatFormat.java index 15dbad0d..31d20b80 100644 --- a/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/schema/format/FloatFormat.java +++ b/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/schema/format/FloatFormat.java @@ -1,12 +1,14 @@ package com.atlassian.oai.validator.schema.format; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.NumericNode; import com.networknt.schema.ExecutionContext; import com.networknt.schema.SchemaContext; import com.networknt.schema.format.Format; import com.networknt.schema.utils.JsonType; import com.networknt.schema.utils.TypeFactory; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.node.NumericNode; + +import java.math.BigDecimal; public class FloatFormat implements Format { @Override @@ -33,8 +35,15 @@ public boolean matches(final ExecutionContext executionContext, final SchemaCont return false; } - final float f = numericValue.floatValue(); - final String original = String.valueOf(numericValue.decimalValue()); + final BigDecimal dec = numericValue.decimalValue(); + // Derive the float from the decimal: BigDecimal#floatValue() yields Infinity on overflow, + // whereas NumericNode#floatValue() throws in Jackson 3 when the value is out of float range. + final float f = dec.floatValue(); + if (Float.isInfinite(f) || Float.isNaN(f)) { + return false; + } + + final String original = String.valueOf(dec); final String parsed = String.valueOf(f); return original.equals(parsed); diff --git a/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/schema/format/Int32Format.java b/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/schema/format/Int32Format.java index 348d0d5f..8ad0e7bf 100644 --- a/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/schema/format/Int32Format.java +++ b/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/schema/format/Int32Format.java @@ -1,11 +1,11 @@ package com.atlassian.oai.validator.schema.format; -import com.fasterxml.jackson.databind.JsonNode; import com.networknt.schema.ExecutionContext; import com.networknt.schema.SchemaContext; import com.networknt.schema.format.Format; import com.networknt.schema.utils.JsonType; import com.networknt.schema.utils.TypeFactory; +import tools.jackson.databind.JsonNode; public class Int32Format implements Format { @Override diff --git a/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/schema/format/Int64Format.java b/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/schema/format/Int64Format.java index 21222a15..388e49ea 100644 --- a/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/schema/format/Int64Format.java +++ b/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/schema/format/Int64Format.java @@ -1,11 +1,11 @@ package com.atlassian.oai.validator.schema.format; -import com.fasterxml.jackson.databind.JsonNode; import com.networknt.schema.ExecutionContext; import com.networknt.schema.SchemaContext; import com.networknt.schema.format.Format; import com.networknt.schema.utils.JsonType; import com.networknt.schema.utils.TypeFactory; +import tools.jackson.databind.JsonNode; public class Int64Format implements Format { @Override diff --git a/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/util/OpenApiLoader.java b/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/util/OpenApiLoader.java index 6adabba0..cc6b2084 100644 --- a/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/util/OpenApiLoader.java +++ b/openapi-request-validator-core/src/main/java/com/atlassian/oai/validator/util/OpenApiLoader.java @@ -136,7 +136,7 @@ private static void removeTypeObjectAssociationWithOneOfAndAnyOfFromSchema(@Nonn * Removes the Base64 pattern on the {@link OpenAPI} model. *

* If that pattern would stay on the model all fields of type string / byte would be validated twice. Once - * with the {@link com.networknt.schema.PatternValidator} and once with + * with the {@link com.networknt.schema.keyword.PatternValidator} and once with * the {@link com.atlassian.oai.validator.schema.format.Base64Format}. * To improve validation performance and memory footprint the pattern on string / byte fields will be * removed - so the PatternValidator will not be triggered for those kind of fields. diff --git a/openapi-request-validator-core/src/test/java/com/atlassian/oai/validator/schema/SchemaValidatorTest.java b/openapi-request-validator-core/src/test/java/com/atlassian/oai/validator/schema/SchemaValidatorTest.java index ac31dcff..87342eee 100644 --- a/openapi-request-validator-core/src/test/java/com/atlassian/oai/validator/schema/SchemaValidatorTest.java +++ b/openapi-request-validator-core/src/test/java/com/atlassian/oai/validator/schema/SchemaValidatorTest.java @@ -165,8 +165,11 @@ public void validate_withInvalidDoubleFormat_shouldFail() { final String value = "1" + Double.MAX_VALUE; final Schema schema = new NumberSchema().format("double"); + // A value outside the double range cannot be represented as a JSON number that the + // validator accepts: networknt 3.x rejects it as a type error ("number expected") before + // the custom double-format validator is reached. The value is still rejected. assertFailWithoutContext(classUnderTest.validate(value, schema, "prefix"), - "validation.prefix.schema.format.double"); + "validation.prefix.schema.type"); } @Test diff --git a/openapi-request-validator-core/src/test/java/com/atlassian/oai/validator/v31analysis/NetworkntDirectProbeTest.java b/openapi-request-validator-core/src/test/java/com/atlassian/oai/validator/v31analysis/NetworkntDirectProbeTest.java index 1979ebc3..278433b9 100644 --- a/openapi-request-validator-core/src/test/java/com/atlassian/oai/validator/v31analysis/NetworkntDirectProbeTest.java +++ b/openapi-request-validator-core/src/test/java/com/atlassian/oai/validator/v31analysis/NetworkntDirectProbeTest.java @@ -1,6 +1,6 @@ package com.atlassian.oai.validator.v31analysis; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.networknt.schema.InputFormat; import com.networknt.schema.SchemaRegistry; import com.networknt.schema.dialect.Dialects; import org.junit.jupiter.api.Test; @@ -19,13 +19,11 @@ public class NetworkntDirectProbeTest { private static final Logger LOG = LoggerFactory.getLogger(NetworkntDirectProbeTest.class); - private static final ObjectMapper MAPPER = new ObjectMapper(); private static final SchemaRegistry REGISTRY = SchemaRegistry.withDefaultDialect(Dialects.getOpenApi31()); private static int validate(final String schemaJson, final String input) throws Exception { final var schema = REGISTRY.getSchema(schemaJson); - final var node = MAPPER.readTree(input); - final var errs = schema.validate(node); + final var errs = schema.validate(input, InputFormat.JSON); errs.forEach(e -> LOG.error(" - {}", e)); return errs.size(); } @@ -138,8 +136,7 @@ void discriminator_with_mapping_via_oas31_metaschema() throws Exception { + "}"; final var s = REGISTRY.getSchema(schema); - final var node = MAPPER.readTree("{\"kind\":\"circle\",\"radius\":5}"); - final var errs = s.validate(node); + final var errs = s.validate("{\"kind\":\"circle\",\"radius\":5}", InputFormat.JSON); errs.forEach(e -> LOG.error(" - {}", e)); LOG.error("[direct/discriminator+mapping circle] errors: {}", errs.size()); } diff --git a/openapi-request-validator-core/src/test/java/com/atlassian/oai/validator/v31analysis/UnevaluatedDeepProbeTest.java b/openapi-request-validator-core/src/test/java/com/atlassian/oai/validator/v31analysis/UnevaluatedDeepProbeTest.java index 1a87b43b..ad98f027 100644 --- a/openapi-request-validator-core/src/test/java/com/atlassian/oai/validator/v31analysis/UnevaluatedDeepProbeTest.java +++ b/openapi-request-validator-core/src/test/java/com/atlassian/oai/validator/v31analysis/UnevaluatedDeepProbeTest.java @@ -1,6 +1,6 @@ package com.atlassian.oai.validator.v31analysis; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.networknt.schema.InputFormat; import com.networknt.schema.SchemaRegistry; import com.networknt.schema.dialect.Dialects; import org.junit.jupiter.api.Test; @@ -14,15 +14,13 @@ public class UnevaluatedDeepProbeTest { private static final Logger LOG = LoggerFactory.getLogger(UnevaluatedDeepProbeTest.class); - private static final ObjectMapper MAPPER = new ObjectMapper(); private static int run(final String label, final SchemaRegistry registry, final String schemaJson, final String input) throws Exception { final var schema = registry.getSchema(schemaJson); - final var node = MAPPER.readTree(input); - final var errs = schema.validate(node); + final var errs = schema.validate(input, InputFormat.JSON); LOG.error("[{}] errors: {}", label, errs.size()); errs.forEach(e -> LOG.error(" - {}", e)); return errs.size(); diff --git a/pom.xml b/pom.xml index 481987ae..1ce85005 100644 --- a/pom.xml +++ b/pom.xml @@ -58,7 +58,10 @@ 3.0 1.3.2 2.21.3 - 2.0.1 + + 3.1.1 + 3.0.4 3.0.2 1.5.32 5.23.0 @@ -375,6 +378,13 @@ import pom + + tools.jackson + jackson-bom + ${jackson3.version} + import + pom + javax.servlet javax.servlet-api