Skip to content

Commit a42af59

Browse files
christiangoerdespredic8github-actions[bot]github-actions
authored
Add support for reference schemas in JSON/YAML schema validation (7.X) (#2477)
* 6.x json2xml array support (#2364) * feat: json2xml support for arrays * refactor: minor * refactor: minor * refactor: minor * refactor: minor * refactor: minor * refactor: minor * refactor: minor * refactor: minor * refactor: minor --------- Co-authored-by: Thomas Bayer <bayer@predic8.de> * Restrict JAVA_OPTS path normalization to log4j.configurationFile in start_router.sh (for 6.X) (#2381) * fix javadoc * fix javadoc * Release 6.3.11 (#2384) Co-authored-by: github-actions <github-actions@github.com> * Snapshot version (#2385) Co-authored-by: github-actions <github-actions@github.com> * Add support for reference schemas in JSON/YAML schema validation (infrastructure) * Make `schemas` field private in ReferenceSchemas * Improve logging for unsupported referenceSchemas in schema validation * Refactor schema validation logging and add example for JSON schema with reference mappings * Add tests for JSON schema validation with reference mappings * improvements * Refactor `JSONYAMLSchemaValidator` to use updated schema validation APIs and improve error assertion handling in tests * Add license headers and null check for schemaMappings in ValidatorInterceptor * refactor: simplify JSON Schema validation setup and update factorypath to use version 7.0.5-SNAPSHOT * refactor: replace JsonSchema factory logic with schema registry and enhance format detection for YAML edge cases --------- Co-authored-by: Thomas Bayer <bayer@predic8.de> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions <github-actions@github.com>
1 parent ed83f82 commit a42af59

File tree

19 files changed

+557
-13
lines changed

19 files changed

+557
-13
lines changed

annot/src/main/java/com/predic8/membrane/annot/generator/Scope.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
/* Copyright 2025 predic8 GmbH, www.predic8.com
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License. */
14+
115
package com.predic8.membrane.annot.generator;
216

317
public enum Scope {

annot/src/main/java/com/predic8/membrane/annot/util/ReflectionUtil.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
/* Copyright 2025 predic8 GmbH, www.predic8.com
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License. */
14+
115
package com.predic8.membrane.annot.util;
216

317
public class ReflectionUtil {

annot/src/test/java/com/predic8/membrane/annot/util/ReflectionUtilTest.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
/* Copyright 2025 predic8 GmbH, www.predic8.com
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License. */
14+
115
package com.predic8.membrane.annot.util;
216

317
import org.junit.jupiter.api.*;

core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/ValidatorInterceptor.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ public class ValidatorInterceptor extends AbstractInterceptor implements Applica
6666
private ResolverMap resourceResolver;
6767
private ApplicationContext applicationContext;
6868

69+
private SchemaMappings schemaMappings;
70+
6971
private SOAPProxy soapProxy;
7072

7173
public ValidatorInterceptor() {
@@ -94,15 +96,23 @@ public void init() {
9496

9597
private MessageValidator getMessageValidator() throws Exception {
9698
if (wsdl != null) {
99+
if (schemaMappings != null)
100+
logIgnoringRefSchemas();
97101
return new WSDLValidator(resourceResolver, combine(getBaseLocation(), wsdl), serviceName, createFailureHandler(), skipFaults);
98102
}
99103
if (schema != null) {
104+
if (schemaMappings != null)
105+
logIgnoringRefSchemas();
100106
return new XMLSchemaValidator(resourceResolver, combine(getBaseLocation(), schema), createFailureHandler());
101107
}
102108
if (jsonSchema != null) {
103-
return new JSONYAMLSchemaValidator(resourceResolver, combine(getBaseLocation(), jsonSchema), createFailureHandler(), schemaVersion);
109+
return new JSONYAMLSchemaValidator(resourceResolver, combine(getBaseLocation(), jsonSchema), createFailureHandler(), schemaVersion) {{
110+
if(schemaMappings != null) setSchemaMappings(schemaMappings.getSchemaMap());
111+
}};
104112
}
105113
if (schematron != null) {
114+
if (schemaMappings != null)
115+
logIgnoringRefSchemas();
106116
return new SchematronValidator(combine(getBaseLocation(), schematron), createFailureHandler(), router, applicationContext);
107117
}
108118

@@ -112,6 +122,10 @@ private MessageValidator getMessageValidator() throws Exception {
112122
throw new RuntimeException("Validator is not configured properly. <validator> must have an attribute specifying the validator.");
113123
}
114124

125+
private static void logIgnoringRefSchemas() {
126+
log.warn("Ignoring 'referenceSchemas': schema references are only supported for JSON/YAML validators");
127+
}
128+
115129
private @Nullable WSDLValidator getWsdlValidatorFromSOAPProxy() {
116130
if(soapProxy == null) return null;
117131
wsdl = soapProxy.getWsdl();
@@ -319,6 +333,16 @@ private FailureHandler createFailureHandler() {
319333
throw new IllegalArgumentException("Unknown failureHandler type: " + failureHandler);
320334
}
321335

336+
@MCChildElement
337+
public void setReferenceSchemas(SchemaMappings schemaMappings) {
338+
this.schemaMappings = schemaMappings;
339+
}
340+
341+
public SchemaMappings getReferenceSchemas() {
342+
return schemaMappings;
343+
}
344+
345+
322346
public void setSoapProxy(SOAPProxy soapProxy) {
323347
this.soapProxy = soapProxy;
324348
}

core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONYAMLSchemaValidator.java

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.networknt.schema.*;
2121
import com.networknt.schema.Error;
2222
import com.networknt.schema.path.NodePath;
23+
import com.networknt.schema.resource.SchemaLoader;
2324
import com.predic8.membrane.core.exchange.*;
2425
import com.predic8.membrane.core.interceptor.Interceptor.*;
2526
import com.predic8.membrane.core.interceptor.*;
@@ -48,7 +49,6 @@ public class JSONYAMLSchemaValidator extends AbstractMessageValidator {
4849

4950
private final YAMLFactory factory = YAMLFactory.builder().enable(STRICT_DUPLICATE_DETECTION).build();
5051
private final ObjectMapper yamlObjectMapper = new ObjectMapper(factory);
51-
private final ObjectMapper jsonObjectMapper = new ObjectMapper();
5252

5353
public static final String SCHEMA_VERSION_2020_12 = "2020-12";
5454

@@ -60,10 +60,7 @@ public class JSONYAMLSchemaValidator extends AbstractMessageValidator {
6060
private final AtomicLong invalid = new AtomicLong();
6161
private final SpecificationVersion schemaId;
6262

63-
/**
64-
* JsonSchemaFactory instances are thread-safe provided its configuration is not modified.
65-
*/
66-
SchemaRegistry jsonSchemaFactory;
63+
private Map<String, String> schemaMappings = new HashMap<>();
6764

6865
/**
6966
* JsonSchema instances are thread-safe provided its configuration is not modified.
@@ -76,7 +73,7 @@ public JSONYAMLSchemaValidator(Resolver resolver, String jsonSchema, FailureHand
7673
this.resolver = resolver;
7774
this.jsonSchema = jsonSchema;
7875
this.failureHandler = failureHandler;
79-
this.schemaId = JSONSchemaVersionParser.parse( schemaVersion);
76+
this.schemaId = JSONSchemaVersionParser.parse(schemaVersion);
8077
this.inputFormat = inputFormat;
8178
}
8279

@@ -97,26 +94,34 @@ public String getName() {
9794
public void init() {
9895
super.init();
9996

100-
jsonSchemaFactory = SchemaRegistry.withDefaultDialect(schemaId, builder ->
101-
builder.schemaLoader(loaders -> new MembraneSchemaLoader(resolver)));
102-
10397
try (InputStream in = resolver.resolve(jsonSchema)) {
104-
schema = jsonSchemaFactory.getSchema((jsonSchema.endsWith(".yaml") || jsonSchema.endsWith(".yml") ? yamlObjectMapper: jsonObjectMapper).readTree(in));
98+
schema = createSchemaRegistry().getSchema(SchemaLocation.of(jsonSchema), in, getSchemaFormat());
10599
schema.initializeValidators();
106100
} catch (IOException e) {
107101
throw new RuntimeException("Cannot read JSON Schema from: " + jsonSchema, e);
108102
}
109103
}
110104

105+
private @NotNull InputFormat getSchemaFormat() {
106+
return (jsonSchema.toLowerCase().endsWith(".yaml") || jsonSchema.toLowerCase().endsWith(".yml")) ? YAML : JSON;
107+
}
108+
109+
private SchemaRegistry createSchemaRegistry() {
110+
return SchemaRegistry.withDefaultDialect(schemaId, b -> b.schemaLoader(SchemaLoader.builder()
111+
.schemaIdResolvers(r -> r.mappings(schemaMappings))
112+
.resourceLoaders(rl -> rl.values(list -> list.addFirst(new MembraneSchemaLoader(resolver))))
113+
.build()));
114+
}
115+
111116
public Outcome validateMessage(Exchange exc, Flow flow) throws Exception {
112117
return validateMessage(exc, flow, UTF_8);
113118
}
114119

115120
public Outcome validateMessage(Exchange exc, Flow flow, Charset ignored) throws Exception {
116121

117122
List<Error> assertions = inputFormat == YAML ?
118-
handleMultipleYAMLDocuments(exc, flow) :
119-
schema.validate(exc.getMessage(flow).getBodyAsStringDecoded(), inputFormat);
123+
handleMultipleYAMLDocuments(exc, flow) :
124+
schema.validate(exc.getMessage(flow).getBodyAsStringDecoded(), inputFormat);
120125

121126
if (assertions.isEmpty()) {
122127
valid.incrementAndGet();
@@ -204,4 +209,8 @@ public long getInvalid() {
204209
public String getErrorTitle() {
205210
return "JSON validation failed";
206211
}
212+
213+
public void setSchemaMappings(Map<String, String> schemaMappings) {
214+
this.schemaMappings = schemaMappings;
215+
}
207216
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/* Copyright 2025 predic8 GmbH, www.predic8.com
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License. */
14+
15+
package com.predic8.membrane.core.interceptor.schemavalidation.json;
16+
17+
import com.predic8.membrane.annot.MCAttribute;
18+
import com.predic8.membrane.annot.MCChildElement;
19+
import com.predic8.membrane.annot.MCElement;
20+
import com.predic8.membrane.annot.Required;
21+
22+
import java.util.ArrayList;
23+
import java.util.HashMap;
24+
import java.util.List;
25+
import java.util.Map;
26+
27+
@MCElement(name = "schemaMappings")
28+
public class SchemaMappings {
29+
30+
private List<Schema> schemas = new ArrayList<>();
31+
32+
public Map<String, String> getSchemaMap() {
33+
Map<String, String > referenceSchemas = new HashMap<>();
34+
schemas.forEach(schema -> referenceSchemas.put(schema.getId(), schema.getLocation()));
35+
return referenceSchemas;
36+
}
37+
38+
@Required
39+
@MCChildElement
40+
public void setSchemas(List<Schema> schemas) {
41+
this.schemas = schemas;
42+
}
43+
44+
public List<Schema> getSchemas() {
45+
return schemas;
46+
}
47+
48+
@MCElement(name = "schema")
49+
public static class Schema {
50+
private String id;
51+
52+
private String location;
53+
54+
@MCAttribute
55+
public void setId(String id) {
56+
this.id = id;
57+
}
58+
59+
public String getId() {
60+
return id;
61+
}
62+
63+
@MCAttribute
64+
public void setLocation(String location) {
65+
this.location = location;
66+
}
67+
68+
public String getLocation() {
69+
return location;
70+
}
71+
}
72+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Validation - JSON Schema with Schema Mappings
2+
3+
This sample explains how to set up and use the `validator` plugin with JSON Schemas that reference external schemas
4+
by `$ref` URN/ID, and how to map those references to local files via `schemaMappings`.
5+
6+
## Running the Example
7+
8+
1. Go to the directory:
9+
`<membrane-root>/examples/validation/json-schema/schema-mappings`
10+
11+
2. Start `membrane.cmd` or `membrane.sh`.
12+
13+
3. Look at `schemas/schema2000.json` and note the `$ref` to `urn:app:base_parameter_def`. Then open `schemas/base-param.json` and compare the schema to `good2000.json` and `bad2000.json`.
14+
15+
4. Run `curl -H "Content-Type: application/json" -d @good2000.json http://localhost:2000/` on the console. Observe that you get a successful response.
16+
17+
5. Run `curl -H "Content-Type: application/json" -d @bad2000.json http://localhost:2000/`. Observe that you get a validation error response.
18+
19+
Keeping the router running, you can try a more complex setup with multiple referenced schemas.
20+
21+
1. Have a look at `schemas/schema2001.json` and note the `$ref`s to `urn:app:base_parameter_def` and `urn:app:meta_def`. Then open `schemas/base-param.json` and `schemas/meta.json` and compare the schemas to `good2001.json` and `bad2001.json`.
22+
23+
2. Run `curl -H "Content-Type: application/json" -d @good2001.json http://localhost:2001/`. Observe that you get a successful response.
24+
25+
3. Run `curl -H "Content-Type: application/json" -d @bad2001.json http://localhost:2001/`. Observe that you get a validation error response.
26+
27+
## How it is done
28+
29+
In `proxies.xml`, each API configures a `<validator jsonSchema="...">`.
30+
The root schemas contain `$ref` references to URN/IDs, for example:
31+
32+
- `urn:app:base_parameter_def#/$defs/BaseParameter`
33+
- `urn:app:meta_def#/$defs/Meta`
34+
35+
To let Membrane resolve these URNs, you map them in the validator using:
36+
37+
```xml
38+
<schemaMappings>
39+
<schema id="urn:app:base_parameter_def" location="schemas/base-parameter.json"/>
40+
<schema id="urn:app:meta_def" location="schemas/meta.json"/>
41+
</schemaMappings>
42+
````
43+
44+
Only if validation succeeds, the request is forwarded to the backend (port 2002).
45+
46+
---
47+
48+
See:
49+
50+
* [JSON Schema](https://json-schema.org/) documentation
51+
* [validator](https://www.membrane-api.io/docs/current/validator.html) reference
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"param": {
3+
"name": "limit"
4+
},
5+
"timestamp": "not-a-date",
6+
"extra": true
7+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"params": [
3+
{
4+
"name": "mode",
5+
"value": "fast",
6+
"unexpected": "foo"
7+
}
8+
],
9+
"meta": {
10+
"source": ""
11+
}
12+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"param": {
3+
"name": "limit",
4+
"value": 100
5+
},
6+
"timestamp": "2025-12-19T10:15:30Z"
7+
}

0 commit comments

Comments
 (0)