From ae65d4bf5ec73c9701a9494e8fc05ca7c7169f77 Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Thu, 16 Oct 2025 15:37:42 +0200 Subject: [PATCH 1/4] feat: validator Support for JSON Schema versions 2019-09 and 2020-12 --- core/pom.xml | 5 + .../ValidatorInterceptor.java | 33 +++- .../json/JSONSchemaVersionParser.java | 26 +++ .../json/JSONYAMLSchemaValidator.java | 177 ++++++++++++++++++ .../json/MembraneSchemaLoader.java | 40 ++++ .../JSONSchemaValidationTest.java | 2 +- .../JSONYAMLSchemaValidatorTest.java | 79 ++++++++ .../json/JSONSchemaVersionParserTest.java | 23 +++ .../validation/json-schema/simple-schema.json | 18 ++ .../JSONSchemaValidationExampleTest.java | 98 +++++++--- 10 files changed, 469 insertions(+), 32 deletions(-) create mode 100644 core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONSchemaVersionParser.java create mode 100644 core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONYAMLSchemaValidator.java create mode 100644 core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/MembraneSchemaLoader.java create mode 100644 core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/JSONYAMLSchemaValidatorTest.java create mode 100644 core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONSchemaVersionParserTest.java create mode 100644 core/src/test/resources/validation/json-schema/simple-schema.json diff --git a/core/pom.xml b/core/pom.xml index 9b4264f669..5c14d5aae2 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -169,6 +169,11 @@ oauth2-openid 1.2.0 + + com.networknt + json-schema-validator + 1.5.9 + com.jayway.jsonpath json-path diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/ValidatorInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/ValidatorInterceptor.java index b0d038be08..ce1f8f7e4f 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/ValidatorInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/ValidatorInterceptor.java @@ -17,6 +17,7 @@ import com.predic8.membrane.annot.*; import com.predic8.membrane.core.exchange.*; import com.predic8.membrane.core.interceptor.*; +import com.predic8.membrane.core.interceptor.schemavalidation.json.*; import com.predic8.membrane.core.proxies.*; import com.predic8.membrane.core.resolver.*; import com.predic8.membrane.core.util.*; @@ -32,6 +33,7 @@ import static com.predic8.membrane.core.interceptor.Outcome.ABORT; import static com.predic8.membrane.core.interceptor.Outcome.*; import static com.predic8.membrane.core.resolver.ResolverMap.*; +import static com.predic8.membrane.core.util.TextUtil.linkURL; /** * Basically switches over {@link WSDLValidator}, {@link XMLSchemaValidator}, @@ -49,6 +51,13 @@ public class ValidatorInterceptor extends AbstractInterceptor implements Applica private String schema; private String serviceName; private String jsonSchema; + + /** + * Schema version e.g. JSON Schema version 04, 07, 2020-12 + * Could also be used for XML or WSDL schema versions later. + */ + private String schemaVersion = "2020-12"; + private String schematron; private String failureHandler; private boolean skipFaults; @@ -89,7 +98,7 @@ private MessageValidator getMessageValidator() throws Exception { return new XMLSchemaValidator(resourceResolver, combine(getBaseLocation(), schema), createFailureHandler()); } if (jsonSchema != null) { - return new JSONSchemaValidator(resourceResolver, combine(getBaseLocation(), jsonSchema), createFailureHandler()); + return new JSONYAMLSchemaValidator(resourceResolver, combine(getBaseLocation(), jsonSchema, schemaVersion), createFailureHandler()); } if (schematron != null) { return new SchematronValidator(combine(getBaseLocation(), schematron), createFailureHandler(), router, applicationContext); @@ -206,6 +215,20 @@ public void setJsonSchema(String jsonSchema) { this.jsonSchema = jsonSchema; } + public String getSchemaVersion() { + return schemaVersion; + } + + /** + * @description The version of the Schema. + * @example 04, 05, 06, 07, 2019-09, 2020-12 + * @default 2020-12 + */ + @MCAttribute + public void setSchemaVersion(String version) { + this.schemaVersion = version; + } + public String getSchematron() { return schematron; } @@ -263,19 +286,19 @@ public String getLongDescription() { sb.append(" according to "); if (wsdl != null) { sb.append("the WSDL at
"); - sb.append(TextUtil.linkURL(wsdl)); + sb.append(linkURL(wsdl)); } if (schema != null) { sb.append("the XML Schema at
"); - sb.append(TextUtil.linkURL(schema)); + sb.append(linkURL(schema)); } if (jsonSchema != null) { sb.append("the JSON Schema at
"); - sb.append(TextUtil.linkURL(jsonSchema)); + sb.append(linkURL(jsonSchema)); } if (schematron != null) { sb.append("the Schematron at
"); - sb.append(TextUtil.linkURL(schematron)); + sb.append(linkURL(schematron)); } sb.append(" ."); return sb.toString(); diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONSchemaVersionParser.java b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONSchemaVersionParser.java new file mode 100644 index 0000000000..fddeb5249d --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONSchemaVersionParser.java @@ -0,0 +1,26 @@ +package com.predic8.membrane.core.interceptor.schemavalidation.json; + +import com.networknt.schema.*; +import com.predic8.membrane.core.util.*; + +import static com.networknt.schema.SchemaId.*; +import static com.networknt.schema.SchemaId.V4; +import static com.networknt.schema.SchemaId.V6; + +public class JSONSchemaVersionParser { + + public static SpecVersion.VersionFlag parse(String version) { + return SpecVersion.VersionFlag.fromId(aliasToSpecId(version)).get(); + } + + static String aliasToSpecId(String alias) { + return switch (alias) { + case "04","draft-04" -> V4; + case "06","draft-06" -> V6; + case "07","draft-07" -> V7; + case "2019-09","draft-2019-09" -> V201909; + case "2020-12", "draft-2020-12" -> V202012; + default -> throw new ConfigurationException("Unknown JSON Schema version: " + alias); + }; + } +} diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONYAMLSchemaValidator.java b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONYAMLSchemaValidator.java new file mode 100644 index 0000000000..de170da17f --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONYAMLSchemaValidator.java @@ -0,0 +1,177 @@ +/* Copyright 2012 predic8 GmbH, www.predic8.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + +package com.predic8.membrane.core.interceptor.schemavalidation.json; + +import com.github.fge.jsonschema.*; +import com.networknt.schema.*; +import com.predic8.membrane.core.exchange.*; +import com.predic8.membrane.core.http.*; +import com.predic8.membrane.core.interceptor.Interceptor.*; +import com.predic8.membrane.core.interceptor.*; +import com.predic8.membrane.core.interceptor.schemavalidation.*; +import com.predic8.membrane.core.interceptor.schemavalidation.ValidatorInterceptor.*; +import com.predic8.membrane.core.resolver.*; +import org.jetbrains.annotations.*; +import org.slf4j.*; + +import java.nio.charset.*; +import java.util.*; +import java.util.concurrent.atomic.*; + +import static com.networknt.schema.InputFormat.JSON; +import static com.predic8.membrane.core.exceptions.ProblemDetails.*; +import static com.predic8.membrane.core.interceptor.Outcome.*; +import static java.nio.charset.StandardCharsets.*; + +public class JSONYAMLSchemaValidator extends AbstractMessageValidator { + + private static final Logger log = LoggerFactory.getLogger(JSONYAMLSchemaValidator.class); + + private final Resolver resolver; + private final String jsonSchema; + private final FailureHandler failureHandler; + + private final AtomicLong valid = new AtomicLong(); + private final AtomicLong invalid = new AtomicLong(); + private final SpecVersion.VersionFlag schemaId; + + /** + * JsonSchemaFactory instances are thread-safe provided its configuration is not modified. + */ + JsonSchemaFactory jsonSchemaFactory; + + SchemaValidatorsConfig config; + + /** + * JsonSchema instances are thread-safe provided its configuration is not modified. + */ + JsonSchema schema; + + public JSONYAMLSchemaValidator(Resolver resolver, String jsonSchema, FailureHandler failureHandler, String schemaVersion) { + this.resolver = resolver; + this.jsonSchema = jsonSchema; + this.failureHandler = failureHandler; + this.schemaId = JSONSchemaVersionParser.parse( schemaVersion); + } + + public JSONYAMLSchemaValidator(Resolver resolver, String jsonSchema, FailureHandler failureHandler) { + this(resolver, jsonSchema, failureHandler, "2020-12"); + } + + @Override + public String getName() { + return "JSON Schema Validator"; + } + + @Override + public void init() { + super.init(); + + jsonSchemaFactory = JsonSchemaFactory.getInstance(schemaId, builder -> + builder.schemaLoaders(loaders -> loaders.add(new MembraneSchemaLoader(resolver))) + // builder.schemaMappers(schemaMappers -> schemaMappers.mapPrefix("https://www.example.org/", "classpath:/")) + ); + + SchemaValidatorsConfig.Builder builder = SchemaValidatorsConfig.builder(); + // By default the JDK regular expression implementation which is not ECMA 262 compliant is used + // Note that setting this requires including optional dependencies + // builder.regularExpressionFactory(GraalJSRegularExpressionFactory.getInstance()); + // builder.regularExpressionFactory(JoniRegularExpressionFactory.getInstance()); + config = builder.build(); + + // If the schema data does not specify an $id the absolute IRI of the schema location will be used as the $id. + schema= jsonSchemaFactory.getSchema(SchemaLocation.of( jsonSchema), config); + schema.initializeValidators(); + + } + + public Outcome validateMessage(Exchange exc, Flow flow) throws Exception { + return validateMessage(exc, flow, UTF_8); + } + + public Outcome validateMessage(Exchange exc, Flow flow, Charset ignored) throws Exception { + + Set assertions = schema.validate(exc.getMessage(flow).getBodyAsStringDecoded(), JSON); + + if (assertions.isEmpty()) { + valid.incrementAndGet(); + return CONTINUE; + } + invalid.incrementAndGet(); + + log.debug("Validation failed: {}", assertions); + + user(false, getName()) + .title(getErrorTitle()) + .addSubType("validation") + .component(getName()) + .internal("flow", flow.name()) + .internal("errors", getMapForProblemDetails(assertions)) + .buildAndSetResponse(exc); + + return ABORT; + } + + private @NotNull List> getMapForProblemDetails(Set assertions) { + return assertions.stream().map(this::validationMessageToProblemDetailsMap).toList(); + } + + private @NotNull Map validationMessageToProblemDetailsMap(ValidationMessage vm) { + Map m = new LinkedHashMap<>(); + m.put("message", vm.getMessage()); + m.put("code", vm.getCode()); + m.put("key", vm.getMessageKey()); + if (vm.getDetails() != null) + m.put("details", vm.getDetails()); + m.put("type", vm.getType()); + m.put("error", vm.getError()); + m.put("pointer", getPointer(vm.getEvaluationPath())); + m.put("node", vm.getInstanceNode()); + return m; + } + + private String getPointer(JsonNodePath evaluationPath) { + if (evaluationPath == null || evaluationPath.getNameCount() == 0) { + return ""; + } + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < evaluationPath.getNameCount(); i++) { + sb.append('/'); + String part = evaluationPath.getName(i); + + // escape according to RFC 6901 + part = part.replace("~", "~0").replace("/", "~1"); + + sb.append(part); + } + return sb.toString(); + } + + @Override + public long getValid() { + return valid.get(); + } + + @Override + public long getInvalid() { + return invalid.get(); + } + + @Override + public String getErrorTitle() { + return "JSON validation failed"; + } +} \ No newline at end of file diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/MembraneSchemaLoader.java b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/MembraneSchemaLoader.java new file mode 100644 index 0000000000..659f27cf23 --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/MembraneSchemaLoader.java @@ -0,0 +1,40 @@ +/* Copyright 2025 predic8 GmbH, www.predic8.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + +package com.predic8.membrane.core.interceptor.schemavalidation.json; + +import com.networknt.schema.*; +import com.networknt.schema.resource.*; +import com.predic8.membrane.core.resolver.*; + +public class MembraneSchemaLoader implements SchemaLoader { + + Resolver resolver; + + public MembraneSchemaLoader(Resolver resolver) { + this.resolver = resolver; + } + + @Override + public InputStreamSource getSchema(AbsoluteIri absoluteIri) { + + + System.out.println("absoluteIri = " + absoluteIri); + System.out.println("absoluteIri = " + absoluteIri.toString()); + + return () -> resolver.resolve(absoluteIri.toString()); + + + } +} diff --git a/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/JSONSchemaValidationTest.java b/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/JSONSchemaValidationTest.java index 43163282f9..9f34f84ba6 100644 --- a/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/JSONSchemaValidationTest.java +++ b/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/JSONSchemaValidationTest.java @@ -79,7 +79,7 @@ void inValid1() throws Exception { assertEquals("https://membrane-api.io/problems/user/validation",jn.get("type").textValue()); assertEquals(1, jn.get("errors").size()); -// System.out.println("exc.getResponse().getBodyAsStringDecoded() = " + exc.getResponse().getBodyAsStringDecoded()); + System.out.println("exc.getResponse().getBodyAsStringDecoded() = " + exc.getResponse().getBodyAsStringDecoded()); } @Test diff --git a/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/JSONYAMLSchemaValidatorTest.java b/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/JSONYAMLSchemaValidatorTest.java new file mode 100644 index 0000000000..9843b62c13 --- /dev/null +++ b/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/JSONYAMLSchemaValidatorTest.java @@ -0,0 +1,79 @@ +/* Copyright 2025 predic8 GmbH, www.predic8.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + +package com.predic8.membrane.core.interceptor.schemavalidation; + +import com.predic8.membrane.core.exchange.*; +import com.predic8.membrane.core.interceptor.schemavalidation.json.*; +import com.predic8.membrane.core.resolver.*; +import com.predic8.membrane.core.util.*; +import org.junit.jupiter.api.*; + +import static com.predic8.membrane.core.http.Request.get; +import static com.predic8.membrane.core.interceptor.Interceptor.Flow.REQUEST; +import static com.predic8.membrane.core.interceptor.Outcome.CONTINUE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class JSONYAMLSchemaValidatorTest { + + JSONYAMLSchemaValidator validator; + + @BeforeEach + void setup() { + validator = new JSONYAMLSchemaValidator(new ClasspathSchemaResolver(), "/validation/json-schema/simple-schema.json", null); + validator.init(); + } + + @Test + void invalidSchemaVersion() { + assertThrows(ConfigurationException.class, () -> + new JSONYAMLSchemaValidator(new ClasspathSchemaResolver(), "doesn't matter", null,"unknown version")); + } + + @Test + void simple() throws Exception { + Exchange exc = get("/foo").body(""" + { + "name": "Robert" + } + """).buildExchange(); + assertEquals(CONTINUE, validator.validateMessage( exc, REQUEST)); + assertEquals(1, validator.getValid()); + } + + @Test + void invalidNumber() throws Exception { + Exchange exc = get("/foo").body(""" + { + "age": -1 + } + """).buildExchange(); + validator.validateMessage( exc, REQUEST); + assertEquals(1, validator.getInvalid()); + assertEquals(0, validator.getValid()); + System.out.println("exc.getResponse().getBodyAsStringDecoded() = " + exc.getResponse().getBodyAsStringDecoded()); + } + + @Test + void additionalProperty() throws Exception { + Exchange exc = get("/foo").body(""" + { + "unknown": "foo" + } + """).buildExchange(); + validator.validateMessage( exc, REQUEST); + System.out.println("exc.getResponse().getBodyAsStringDecoded() = " + exc.getResponse().getBodyAsStringDecoded()); + } +} \ No newline at end of file diff --git a/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONSchemaVersionParserTest.java b/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONSchemaVersionParserTest.java new file mode 100644 index 0000000000..11a9c9f9ec --- /dev/null +++ b/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONSchemaVersionParserTest.java @@ -0,0 +1,23 @@ +package com.predic8.membrane.core.interceptor.schemavalidation.json; + +import com.networknt.schema.*; +import org.junit.jupiter.api.*; + +import static com.networknt.schema.SpecVersion.VersionFlag.*; +import static com.predic8.membrane.core.interceptor.schemavalidation.json.JSONSchemaVersionParser.parse; +import static org.junit.jupiter.api.Assertions.*; + +class JSONSchemaVersionParserTest { + + @Test + void parseFromAlias() { + assertEquals(V4, parse("04")); + assertEquals(V6, parse("06")); + assertEquals(V7, parse("07")); + assertEquals(V4, parse("draft-04")); + assertEquals(V6, parse("draft-06")); + assertEquals(V7, parse("draft-07")); + assertEquals(V201909, parse("2019-09")); + assertEquals(V202012, parse("2020-12")); + } +} \ No newline at end of file diff --git a/core/src/test/resources/validation/json-schema/simple-schema.json b/core/src/test/resources/validation/json-schema/simple-schema.json new file mode 100644 index 0000000000..84188d3292 --- /dev/null +++ b/core/src/test/resources/validation/json-schema/simple-schema.json @@ -0,0 +1,18 @@ +{ + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "minLength": 3 + }, + "age": { + "type": "integer", + "minimum": 0 + }, + "zip": { + "type": "string", + "pattern": "^\\d{5}$" + } + } +} \ No newline at end of file diff --git a/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/validation/JSONSchemaValidationExampleTest.java b/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/validation/JSONSchemaValidationExampleTest.java index 510f0de6ed..7af7f98b80 100644 --- a/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/validation/JSONSchemaValidationExampleTest.java +++ b/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/validation/JSONSchemaValidationExampleTest.java @@ -14,9 +14,12 @@ package com.predic8.membrane.examples.withoutinternet.validation; +import com.predic8.membrane.core.http.*; import com.predic8.membrane.examples.util.*; +import org.hamcrest.*; import org.junit.jupiter.api.*; +import static com.predic8.membrane.core.http.MimeType.APPLICATION_PROBLEM_JSON; import static io.restassured.RestAssured.given; import static io.restassured.http.ContentType.JSON; import static java.io.File.*; @@ -29,30 +32,73 @@ protected String getExampleDirName() { return "validation" + separator + "json-schema"; } - @Test - public void test() throws Exception { - try(Process2 ignored = startServiceProxyScript()) { - for (int port : new int[] { 2000, 2001 }) { - // @formatter:off - // Test good JSON - given() - .contentType(JSON) - .body(readFileFromBaseDir("good" + port + ".json")) - .when() - .post("http://localhost:" + port + "/") - .then() - .statusCode(200); - - // Test bad JSON - given() - .contentType(JSON) - .body(readFileFromBaseDir("bad" + port + ".json")) - .when() - .post("http://localhost:" + port + "/") - .then() - .statusCode(400); - // @formatter:on - } - } - } + @Test + void port2000() throws Exception { + try(Process2 ignored = startServiceProxyScript()) { + + // @formatter:off + // Test good JSON + given() + .contentType(JSON) + .body(readFileFromBaseDir("good2000.json")) + .when() + .post("http://localhost:2000") + .then() + .statusCode(200); + + // Test bad JSON + given() + .contentType(JSON) + .body(readFileFromBaseDir("bad2000.json")) + .when() + .post("http://localhost:2000") + .then() + .statusCode(400) + .contentType(APPLICATION_PROBLEM_JSON) + .body("title", Matchers.equalTo("JSON validation failed")) + .body("type", Matchers.equalTo("https://membrane-api.io/problems/user/validation")) + .body(Matchers.containsString("not found")) + .body(Matchers.containsString("/required")) + .body(Matchers.containsString("p1")); + // @formatter:on + + } + } + + @Test + void port2001() throws Exception { + try(Process2 ignored = startServiceProxyScript()) { + + // @formatter:off + // Test good JSON + given() + .contentType(JSON) + .body(readFileFromBaseDir("good2001.json")) + .when() + .post("http://localhost:2001") + .then() + .statusCode(200); + + // Test bad JSON + given() + .contentType(JSON) + .body(readFileFromBaseDir("bad2001.json")) + .when() + .post("http://localhost:2001") + .then() + .log().all() + .statusCode(400) + .contentType(APPLICATION_PROBLEM_JSON) + .body("title", Matchers.equalTo("JSON validation failed")) + .body("type", Matchers.equalTo("https://membrane-api.io/problems/user/validation")) + .body(Matchers.containsString("number expected")) + .body(Matchers.containsString("/price")) + .body(Matchers.containsString("array expected")) + .body(Matchers.containsString("/properties/tags/type")) + .body(Matchers.containsString("diesel")) + .body(Matchers.containsString("/price")); + // @formatter:on + + } + } } \ No newline at end of file From 71c61d9201f6f07b5c44e33175644f01db4a2989 Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Thu, 16 Oct 2025 16:20:42 +0200 Subject: [PATCH 2/4] refactor: minor --- .../ValidatorInterceptor.java | 4 +-- .../json/JSONSchemaVersionParser.java | 19 ++++++++++- .../json/JSONYAMLSchemaValidator.java | 6 +++- .../json/MembraneSchemaLoader.java | 9 +---- .../JSONSchemaValidationTest.java | 4 --- .../JSONYAMLSchemaValidatorTest.java | 4 +-- .../json/JSONSchemaVersionParserTest.java | 33 +++++++++++++++++-- .../JSONSchemaValidationExampleTest.java | 9 ++--- docs/ROADMAP.md | 6 ++++ 9 files changed, 67 insertions(+), 27 deletions(-) diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/ValidatorInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/ValidatorInterceptor.java index ce1f8f7e4f..f183ae349d 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/ValidatorInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/ValidatorInterceptor.java @@ -98,7 +98,7 @@ private MessageValidator getMessageValidator() throws Exception { return new XMLSchemaValidator(resourceResolver, combine(getBaseLocation(), schema), createFailureHandler()); } if (jsonSchema != null) { - return new JSONYAMLSchemaValidator(resourceResolver, combine(getBaseLocation(), jsonSchema, schemaVersion), createFailureHandler()); + return new JSONYAMLSchemaValidator(resourceResolver, combine(getBaseLocation(), jsonSchema), createFailureHandler(), schemaVersion); } if (schematron != null) { return new SchematronValidator(combine(getBaseLocation(), schematron), createFailureHandler(), router, applicationContext); @@ -313,7 +313,7 @@ public interface FailureHandler { private FailureHandler createFailureHandler() { if (failureHandler == null || failureHandler.equals("response")) - return null; + return (msg,exchange) -> {}; if (failureHandler.equals("log")) return (message, exc) -> log.info("Validation failure: {}", message); throw new IllegalArgumentException("Unknown failureHandler type: " + failureHandler); diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONSchemaVersionParser.java b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONSchemaVersionParser.java index fddeb5249d..9bc6923300 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONSchemaVersionParser.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONSchemaVersionParser.java @@ -1,7 +1,22 @@ +/* Copyright 2025 predic8 GmbH, www.predic8.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + package com.predic8.membrane.core.interceptor.schemavalidation.json; import com.networknt.schema.*; import com.predic8.membrane.core.util.*; +import org.jetbrains.annotations.*; import static com.networknt.schema.SchemaId.*; import static com.networknt.schema.SchemaId.V4; @@ -13,7 +28,9 @@ public static SpecVersion.VersionFlag parse(String version) { return SpecVersion.VersionFlag.fromId(aliasToSpecId(version)).get(); } - static String aliasToSpecId(String alias) { + static @NotNull String aliasToSpecId(String alias) { + if (alias == null) + throw new ConfigurationException("Unknown JSON Schema version: " + alias); return switch (alias) { case "04","draft-04" -> V4; case "06","draft-06" -> V6; diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONYAMLSchemaValidator.java b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONYAMLSchemaValidator.java index de170da17f..bcf89d2a47 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONYAMLSchemaValidator.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONYAMLSchemaValidator.java @@ -111,14 +111,18 @@ public Outcome validateMessage(Exchange exc, Flow flow, Charset ignored) throws } invalid.incrementAndGet(); + log.debug("Validation failed: {}", assertions); + List> mapForProblemDetails = getMapForProblemDetails(assertions); + failureHandler.handleFailure(mapForProblemDetails.toString(), exc); + user(false, getName()) .title(getErrorTitle()) .addSubType("validation") .component(getName()) .internal("flow", flow.name()) - .internal("errors", getMapForProblemDetails(assertions)) + .internal("errors", mapForProblemDetails) .buildAndSetResponse(exc); return ABORT; diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/MembraneSchemaLoader.java b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/MembraneSchemaLoader.java index 659f27cf23..41a0e400d8 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/MembraneSchemaLoader.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/MembraneSchemaLoader.java @@ -20,7 +20,7 @@ public class MembraneSchemaLoader implements SchemaLoader { - Resolver resolver; + private final Resolver resolver; public MembraneSchemaLoader(Resolver resolver) { this.resolver = resolver; @@ -28,13 +28,6 @@ public MembraneSchemaLoader(Resolver resolver) { @Override public InputStreamSource getSchema(AbsoluteIri absoluteIri) { - - - System.out.println("absoluteIri = " + absoluteIri); - System.out.println("absoluteIri = " + absoluteIri.toString()); - return () -> resolver.resolve(absoluteIri.toString()); - - } } diff --git a/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/JSONSchemaValidationTest.java b/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/JSONSchemaValidationTest.java index 9f34f84ba6..4e0bf3354a 100644 --- a/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/JSONSchemaValidationTest.java +++ b/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/JSONSchemaValidationTest.java @@ -78,8 +78,6 @@ void inValid1() throws Exception { assertEquals("JSON validation failed", jn.get("title").textValue()); assertEquals("https://membrane-api.io/problems/user/validation",jn.get("type").textValue()); assertEquals(1, jn.get("errors").size()); - - System.out.println("exc.getResponse().getBodyAsStringDecoded() = " + exc.getResponse().getBodyAsStringDecoded()); } @Test @@ -147,8 +145,6 @@ void inValid2() throws Exception { assertEquals("JSON validation failed", jn.get("title").textValue()); assertEquals("https://membrane-api.io/problems/user/validation",jn.get("type").textValue()); assertEquals(2, jn.get("errors").size()); - - System.out.println("exc.getResponse().getBodyAsStringDecoded() = " + exc.getResponse().getBodyAsStringDecoded()); } private static @NotNull JSONSchemaValidator getValidator(String schema) { diff --git a/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/JSONYAMLSchemaValidatorTest.java b/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/JSONYAMLSchemaValidatorTest.java index 9843b62c13..3b14d2e5f7 100644 --- a/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/JSONYAMLSchemaValidatorTest.java +++ b/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/JSONYAMLSchemaValidatorTest.java @@ -32,7 +32,7 @@ class JSONYAMLSchemaValidatorTest { @BeforeEach void setup() { - validator = new JSONYAMLSchemaValidator(new ClasspathSchemaResolver(), "/validation/json-schema/simple-schema.json", null); + validator = new JSONYAMLSchemaValidator(new ClasspathSchemaResolver(), "/validation/json-schema/simple-schema.json", (a,b) -> {}); validator.init(); } @@ -63,7 +63,6 @@ void invalidNumber() throws Exception { validator.validateMessage( exc, REQUEST); assertEquals(1, validator.getInvalid()); assertEquals(0, validator.getValid()); - System.out.println("exc.getResponse().getBodyAsStringDecoded() = " + exc.getResponse().getBodyAsStringDecoded()); } @Test @@ -74,6 +73,5 @@ void additionalProperty() throws Exception { } """).buildExchange(); validator.validateMessage( exc, REQUEST); - System.out.println("exc.getResponse().getBodyAsStringDecoded() = " + exc.getResponse().getBodyAsStringDecoded()); } } \ No newline at end of file diff --git a/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONSchemaVersionParserTest.java b/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONSchemaVersionParserTest.java index 11a9c9f9ec..1474d887da 100644 --- a/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONSchemaVersionParserTest.java +++ b/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONSchemaVersionParserTest.java @@ -1,14 +1,43 @@ +/* Copyright 2025 predic8 GmbH, www.predic8.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + package com.predic8.membrane.core.interceptor.schemavalidation.json; -import com.networknt.schema.*; +import com.predic8.membrane.core.util.*; import org.junit.jupiter.api.*; import static com.networknt.schema.SpecVersion.VersionFlag.*; -import static com.predic8.membrane.core.interceptor.schemavalidation.json.JSONSchemaVersionParser.parse; +import static com.predic8.membrane.core.interceptor.schemavalidation.json.JSONSchemaVersionParser.*; import static org.junit.jupiter.api.Assertions.*; class JSONSchemaVersionParserTest { + @Test + void parseUnknownVersion() { + assertThrows(ConfigurationException.class, () -> parse("invalid-version")); + } + + @Test + void parseNullVersion() { + assertThrows(ConfigurationException.class, () -> parse(null)); + } + + @Test + void parseDraft2019() { + assertEquals(V201909, parse("draft-2019-09")); + } + @Test void parseFromAlias() { assertEquals(V4, parse("04")); diff --git a/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/validation/JSONSchemaValidationExampleTest.java b/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/validation/JSONSchemaValidationExampleTest.java index 7af7f98b80..6f4b9a3633 100644 --- a/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/validation/JSONSchemaValidationExampleTest.java +++ b/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/validation/JSONSchemaValidationExampleTest.java @@ -14,16 +14,14 @@ package com.predic8.membrane.examples.withoutinternet.validation; -import com.predic8.membrane.core.http.*; import com.predic8.membrane.examples.util.*; import org.hamcrest.*; import org.junit.jupiter.api.*; -import static com.predic8.membrane.core.http.MimeType.APPLICATION_PROBLEM_JSON; -import static io.restassured.RestAssured.given; -import static io.restassured.http.ContentType.JSON; +import static com.predic8.membrane.core.http.MimeType.*; +import static io.restassured.RestAssured.*; +import static io.restassured.http.ContentType.*; import static java.io.File.*; -import static java.lang.Thread.sleep; public class JSONSchemaValidationExampleTest extends DistributionExtractingTestcase { @@ -86,7 +84,6 @@ void port2001() throws Exception { .when() .post("http://localhost:2001") .then() - .log().all() .statusCode(400) .contentType(APPLICATION_PROBLEM_JSON) .body("title", Matchers.equalTo("JSON validation failed")) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index fd817c22fa..a88128d7cf 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -12,6 +12,9 @@ - LogInterceptor: - Remove: headerOnly - Rewrite JSONAssert Tests with RESTAssured +- ValidatorInterceptor: remove FailureHandler + - It is often called and only used for logging. + - Put logging in validators # 6.5.0 @@ -42,6 +45,9 @@ - getEndSessionEndpoint() throws Exception - doDynamicRegistration(List callbackURLs) throws Exception +Release Notes: +- JSON Schema validation support for schema versions 2019-09 and 2020-12 + # 6.3.0 - Convert to UTF-8 source and outputEncoding to UTF-8 (TB) From 4e1b13e96ca189a88d615d66ddf418f425ec4aa9 Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Fri, 17 Oct 2025 08:54:55 +0200 Subject: [PATCH 3/4] refactor: minor --- .../json/JSONSchemaVersionParser.java | 2 -- .../JSONSchemaValidationExampleTest.java | 28 ++++++++++--------- docs/ROADMAP.md | 10 ++++--- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONSchemaVersionParser.java b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONSchemaVersionParser.java index 9bc6923300..55930be3d4 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONSchemaVersionParser.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONSchemaVersionParser.java @@ -19,8 +19,6 @@ import org.jetbrains.annotations.*; import static com.networknt.schema.SchemaId.*; -import static com.networknt.schema.SchemaId.V4; -import static com.networknt.schema.SchemaId.V6; public class JSONSchemaVersionParser { diff --git a/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/validation/JSONSchemaValidationExampleTest.java b/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/validation/JSONSchemaValidationExampleTest.java index 6f4b9a3633..868aa5764e 100644 --- a/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/validation/JSONSchemaValidationExampleTest.java +++ b/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/validation/JSONSchemaValidationExampleTest.java @@ -22,6 +22,8 @@ import static io.restassured.RestAssured.*; import static io.restassured.http.ContentType.*; import static java.io.File.*; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; public class JSONSchemaValidationExampleTest extends DistributionExtractingTestcase { @@ -53,11 +55,12 @@ void port2000() throws Exception { .then() .statusCode(400) .contentType(APPLICATION_PROBLEM_JSON) - .body("title", Matchers.equalTo("JSON validation failed")) - .body("type", Matchers.equalTo("https://membrane-api.io/problems/user/validation")) - .body(Matchers.containsString("not found")) - .body(Matchers.containsString("/required")) - .body(Matchers.containsString("p1")); + .body("title", equalTo("JSON validation failed")) + .body("type", equalTo("https://membrane-api.io/problems/user/validation")) + .body(containsString("not found")) + .body(containsString("/required")) + .body(containsString("p1")) + .body("errors.find { it.pointer == '/required' }.message", containsString("not found")); // @formatter:on } @@ -86,14 +89,13 @@ void port2001() throws Exception { .then() .statusCode(400) .contentType(APPLICATION_PROBLEM_JSON) - .body("title", Matchers.equalTo("JSON validation failed")) - .body("type", Matchers.equalTo("https://membrane-api.io/problems/user/validation")) - .body(Matchers.containsString("number expected")) - .body(Matchers.containsString("/price")) - .body(Matchers.containsString("array expected")) - .body(Matchers.containsString("/properties/tags/type")) - .body(Matchers.containsString("diesel")) - .body(Matchers.containsString("/price")); + .log().all() + .body("title", equalTo("JSON validation failed")) + .body("type", equalTo("https://membrane-api.io/problems/user/validation")) + .body("errors.find { it.pointer == '/properties/id/type' }.message", containsString("integer expected")) + .body("errors.find { it.pointer == '/properties/price/type' }.message", containsString("number expected")) + .body("errors.find { it.pointer == '/properties/tags/type' }.message", containsString("array expected")) + .body("errors.find { it.pointer == '/properties/weight/minimum' }.message", containsString("700")); // @formatter:on } diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index a88128d7cf..bc4b0a1dce 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -13,8 +13,9 @@ - Remove: headerOnly - Rewrite JSONAssert Tests with RESTAssured - ValidatorInterceptor: remove FailureHandler - - It is often called and only used for logging. - - Put logging in validators + - Predominantly used for logging; move logging into validators. + - Migration: replace FailureHandler usages with validator-level logging; ensure correlation IDs/Exchange context remain available for logs. + - Check if it is used by customer installations # 6.5.0 @@ -44,9 +45,10 @@ - public abstract void init() throws Exception; - getEndSessionEndpoint() throws Exception - doDynamicRegistration(List callbackURLs) throws Exception +## Release Notes: -Release Notes: -- JSON Schema validation support for schema versions 2019-09 and 2020-12 +- JSON Schema validation support for JSON Schema 2019-09 and 2020-12 (via networknt json-schema-validator). + - Document how to select the schema version (e.g., schemaVersion attribute) and the "format" behavior (annotation vs assertion), with a link to usage docs/examples. # 6.3.0 From becd94e147122d4faafb100fb37898fa24002693 Mon Sep 17 00:00:00 2001 From: Thomas Bayer Date: Fri, 17 Oct 2025 09:32:56 +0200 Subject: [PATCH 4/4] refactor: minor --- .../schemavalidation/json/JSONSchemaVersionParser.java | 4 ++-- .../schemavalidation/json/JSONSchemaVersionParserTest.java | 5 ----- .../validation/JSONSchemaValidationExampleTest.java | 7 +------ 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONSchemaVersionParser.java b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONSchemaVersionParser.java index 55930be3d4..c5fe867ebc 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONSchemaVersionParser.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONSchemaVersionParser.java @@ -33,8 +33,8 @@ public static SpecVersion.VersionFlag parse(String version) { case "04","draft-04" -> V4; case "06","draft-06" -> V6; case "07","draft-07" -> V7; - case "2019-09","draft-2019-09" -> V201909; - case "2020-12", "draft-2020-12" -> V202012; + case "2019-09" -> V201909; + case "2020-12" -> V202012; default -> throw new ConfigurationException("Unknown JSON Schema version: " + alias); }; } diff --git a/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONSchemaVersionParserTest.java b/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONSchemaVersionParserTest.java index 1474d887da..ddfec39971 100644 --- a/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONSchemaVersionParserTest.java +++ b/core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONSchemaVersionParserTest.java @@ -33,11 +33,6 @@ void parseNullVersion() { assertThrows(ConfigurationException.class, () -> parse(null)); } - @Test - void parseDraft2019() { - assertEquals(V201909, parse("draft-2019-09")); - } - @Test void parseFromAlias() { assertEquals(V4, parse("04")); diff --git a/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/validation/JSONSchemaValidationExampleTest.java b/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/validation/JSONSchemaValidationExampleTest.java index 868aa5764e..3f3df6cef0 100644 --- a/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/validation/JSONSchemaValidationExampleTest.java +++ b/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/validation/JSONSchemaValidationExampleTest.java @@ -15,15 +15,13 @@ package com.predic8.membrane.examples.withoutinternet.validation; import com.predic8.membrane.examples.util.*; -import org.hamcrest.*; import org.junit.jupiter.api.*; import static com.predic8.membrane.core.http.MimeType.*; import static io.restassured.RestAssured.*; import static io.restassured.http.ContentType.*; import static java.io.File.*; -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.*; public class JSONSchemaValidationExampleTest extends DistributionExtractingTestcase { @@ -57,8 +55,6 @@ void port2000() throws Exception { .contentType(APPLICATION_PROBLEM_JSON) .body("title", equalTo("JSON validation failed")) .body("type", equalTo("https://membrane-api.io/problems/user/validation")) - .body(containsString("not found")) - .body(containsString("/required")) .body(containsString("p1")) .body("errors.find { it.pointer == '/required' }.message", containsString("not found")); // @formatter:on @@ -89,7 +85,6 @@ void port2001() throws Exception { .then() .statusCode(400) .contentType(APPLICATION_PROBLEM_JSON) - .log().all() .body("title", equalTo("JSON validation failed")) .body("type", equalTo("https://membrane-api.io/problems/user/validation")) .body("errors.find { it.pointer == '/properties/id/type' }.message", containsString("integer expected"))