Skip to content

Commit ae65d4b

Browse files
committed
feat: validator Support for JSON Schema versions 2019-09 and 2020-12
1 parent dad2e0b commit ae65d4b

10 files changed

Lines changed: 469 additions & 32 deletions

File tree

core/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,11 @@
169169
<artifactId>oauth2-openid</artifactId>
170170
<version>1.2.0</version>
171171
</dependency>
172+
<dependency>
173+
<groupId>com.networknt</groupId>
174+
<artifactId>json-schema-validator</artifactId>
175+
<version>1.5.9</version>
176+
</dependency>
172177
<dependency>
173178
<groupId>com.jayway.jsonpath</groupId>
174179
<artifactId>json-path</artifactId>

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

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import com.predic8.membrane.annot.*;
1818
import com.predic8.membrane.core.exchange.*;
1919
import com.predic8.membrane.core.interceptor.*;
20+
import com.predic8.membrane.core.interceptor.schemavalidation.json.*;
2021
import com.predic8.membrane.core.proxies.*;
2122
import com.predic8.membrane.core.resolver.*;
2223
import com.predic8.membrane.core.util.*;
@@ -32,6 +33,7 @@
3233
import static com.predic8.membrane.core.interceptor.Outcome.ABORT;
3334
import static com.predic8.membrane.core.interceptor.Outcome.*;
3435
import static com.predic8.membrane.core.resolver.ResolverMap.*;
36+
import static com.predic8.membrane.core.util.TextUtil.linkURL;
3537

3638
/**
3739
* Basically switches over {@link WSDLValidator}, {@link XMLSchemaValidator},
@@ -49,6 +51,13 @@ public class ValidatorInterceptor extends AbstractInterceptor implements Applica
4951
private String schema;
5052
private String serviceName;
5153
private String jsonSchema;
54+
55+
/**
56+
* Schema version e.g. JSON Schema version 04, 07, 2020-12
57+
* Could also be used for XML or WSDL schema versions later.
58+
*/
59+
private String schemaVersion = "2020-12";
60+
5261
private String schematron;
5362
private String failureHandler;
5463
private boolean skipFaults;
@@ -89,7 +98,7 @@ private MessageValidator getMessageValidator() throws Exception {
8998
return new XMLSchemaValidator(resourceResolver, combine(getBaseLocation(), schema), createFailureHandler());
9099
}
91100
if (jsonSchema != null) {
92-
return new JSONSchemaValidator(resourceResolver, combine(getBaseLocation(), jsonSchema), createFailureHandler());
101+
return new JSONYAMLSchemaValidator(resourceResolver, combine(getBaseLocation(), jsonSchema, schemaVersion), createFailureHandler());
93102
}
94103
if (schematron != null) {
95104
return new SchematronValidator(combine(getBaseLocation(), schematron), createFailureHandler(), router, applicationContext);
@@ -206,6 +215,20 @@ public void setJsonSchema(String jsonSchema) {
206215
this.jsonSchema = jsonSchema;
207216
}
208217

218+
public String getSchemaVersion() {
219+
return schemaVersion;
220+
}
221+
222+
/**
223+
* @description The version of the Schema.
224+
* @example 04, 05, 06, 07, 2019-09, 2020-12
225+
* @default 2020-12
226+
*/
227+
@MCAttribute
228+
public void setSchemaVersion(String version) {
229+
this.schemaVersion = version;
230+
}
231+
209232
public String getSchematron() {
210233
return schematron;
211234
}
@@ -263,19 +286,19 @@ public String getLongDescription() {
263286
sb.append(" according to ");
264287
if (wsdl != null) {
265288
sb.append("the WSDL at <br/>");
266-
sb.append(TextUtil.linkURL(wsdl));
289+
sb.append(linkURL(wsdl));
267290
}
268291
if (schema != null) {
269292
sb.append("the XML Schema at <br/>");
270-
sb.append(TextUtil.linkURL(schema));
293+
sb.append(linkURL(schema));
271294
}
272295
if (jsonSchema != null) {
273296
sb.append("the JSON Schema at <br/>");
274-
sb.append(TextUtil.linkURL(jsonSchema));
297+
sb.append(linkURL(jsonSchema));
275298
}
276299
if (schematron != null) {
277300
sb.append("the Schematron at <br/>");
278-
sb.append(TextUtil.linkURL(schematron));
301+
sb.append(linkURL(schematron));
279302
}
280303
sb.append(" .");
281304
return sb.toString();
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.predic8.membrane.core.interceptor.schemavalidation.json;
2+
3+
import com.networknt.schema.*;
4+
import com.predic8.membrane.core.util.*;
5+
6+
import static com.networknt.schema.SchemaId.*;
7+
import static com.networknt.schema.SchemaId.V4;
8+
import static com.networknt.schema.SchemaId.V6;
9+
10+
public class JSONSchemaVersionParser {
11+
12+
public static SpecVersion.VersionFlag parse(String version) {
13+
return SpecVersion.VersionFlag.fromId(aliasToSpecId(version)).get();
14+
}
15+
16+
static String aliasToSpecId(String alias) {
17+
return switch (alias) {
18+
case "04","draft-04" -> V4;
19+
case "06","draft-06" -> V6;
20+
case "07","draft-07" -> V7;
21+
case "2019-09","draft-2019-09" -> V201909;
22+
case "2020-12", "draft-2020-12" -> V202012;
23+
default -> throw new ConfigurationException("Unknown JSON Schema version: " + alias);
24+
};
25+
}
26+
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/* Copyright 2012 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.github.fge.jsonschema.*;
18+
import com.networknt.schema.*;
19+
import com.predic8.membrane.core.exchange.*;
20+
import com.predic8.membrane.core.http.*;
21+
import com.predic8.membrane.core.interceptor.Interceptor.*;
22+
import com.predic8.membrane.core.interceptor.*;
23+
import com.predic8.membrane.core.interceptor.schemavalidation.*;
24+
import com.predic8.membrane.core.interceptor.schemavalidation.ValidatorInterceptor.*;
25+
import com.predic8.membrane.core.resolver.*;
26+
import org.jetbrains.annotations.*;
27+
import org.slf4j.*;
28+
29+
import java.nio.charset.*;
30+
import java.util.*;
31+
import java.util.concurrent.atomic.*;
32+
33+
import static com.networknt.schema.InputFormat.JSON;
34+
import static com.predic8.membrane.core.exceptions.ProblemDetails.*;
35+
import static com.predic8.membrane.core.interceptor.Outcome.*;
36+
import static java.nio.charset.StandardCharsets.*;
37+
38+
public class JSONYAMLSchemaValidator extends AbstractMessageValidator {
39+
40+
private static final Logger log = LoggerFactory.getLogger(JSONYAMLSchemaValidator.class);
41+
42+
private final Resolver resolver;
43+
private final String jsonSchema;
44+
private final FailureHandler failureHandler;
45+
46+
private final AtomicLong valid = new AtomicLong();
47+
private final AtomicLong invalid = new AtomicLong();
48+
private final SpecVersion.VersionFlag schemaId;
49+
50+
/**
51+
* JsonSchemaFactory instances are thread-safe provided its configuration is not modified.
52+
*/
53+
JsonSchemaFactory jsonSchemaFactory;
54+
55+
SchemaValidatorsConfig config;
56+
57+
/**
58+
* JsonSchema instances are thread-safe provided its configuration is not modified.
59+
*/
60+
JsonSchema schema;
61+
62+
public JSONYAMLSchemaValidator(Resolver resolver, String jsonSchema, FailureHandler failureHandler, String schemaVersion) {
63+
this.resolver = resolver;
64+
this.jsonSchema = jsonSchema;
65+
this.failureHandler = failureHandler;
66+
this.schemaId = JSONSchemaVersionParser.parse( schemaVersion);
67+
}
68+
69+
public JSONYAMLSchemaValidator(Resolver resolver, String jsonSchema, FailureHandler failureHandler) {
70+
this(resolver, jsonSchema, failureHandler, "2020-12");
71+
}
72+
73+
@Override
74+
public String getName() {
75+
return "JSON Schema Validator";
76+
}
77+
78+
@Override
79+
public void init() {
80+
super.init();
81+
82+
jsonSchemaFactory = JsonSchemaFactory.getInstance(schemaId, builder ->
83+
builder.schemaLoaders(loaders -> loaders.add(new MembraneSchemaLoader(resolver)))
84+
// builder.schemaMappers(schemaMappers -> schemaMappers.mapPrefix("https://www.example.org/", "classpath:/"))
85+
);
86+
87+
SchemaValidatorsConfig.Builder builder = SchemaValidatorsConfig.builder();
88+
// By default the JDK regular expression implementation which is not ECMA 262 compliant is used
89+
// Note that setting this requires including optional dependencies
90+
// builder.regularExpressionFactory(GraalJSRegularExpressionFactory.getInstance());
91+
// builder.regularExpressionFactory(JoniRegularExpressionFactory.getInstance());
92+
config = builder.build();
93+
94+
// If the schema data does not specify an $id the absolute IRI of the schema location will be used as the $id.
95+
schema= jsonSchemaFactory.getSchema(SchemaLocation.of( jsonSchema), config);
96+
schema.initializeValidators();
97+
98+
}
99+
100+
public Outcome validateMessage(Exchange exc, Flow flow) throws Exception {
101+
return validateMessage(exc, flow, UTF_8);
102+
}
103+
104+
public Outcome validateMessage(Exchange exc, Flow flow, Charset ignored) throws Exception {
105+
106+
Set<ValidationMessage> assertions = schema.validate(exc.getMessage(flow).getBodyAsStringDecoded(), JSON);
107+
108+
if (assertions.isEmpty()) {
109+
valid.incrementAndGet();
110+
return CONTINUE;
111+
}
112+
invalid.incrementAndGet();
113+
114+
log.debug("Validation failed: {}", assertions);
115+
116+
user(false, getName())
117+
.title(getErrorTitle())
118+
.addSubType("validation")
119+
.component(getName())
120+
.internal("flow", flow.name())
121+
.internal("errors", getMapForProblemDetails(assertions))
122+
.buildAndSetResponse(exc);
123+
124+
return ABORT;
125+
}
126+
127+
private @NotNull List<Map<String, Object>> getMapForProblemDetails(Set<ValidationMessage> assertions) {
128+
return assertions.stream().map(this::validationMessageToProblemDetailsMap).toList();
129+
}
130+
131+
private @NotNull Map<String, Object> validationMessageToProblemDetailsMap(ValidationMessage vm) {
132+
Map<String, Object> m = new LinkedHashMap<>();
133+
m.put("message", vm.getMessage());
134+
m.put("code", vm.getCode());
135+
m.put("key", vm.getMessageKey());
136+
if (vm.getDetails() != null)
137+
m.put("details", vm.getDetails());
138+
m.put("type", vm.getType());
139+
m.put("error", vm.getError());
140+
m.put("pointer", getPointer(vm.getEvaluationPath()));
141+
m.put("node", vm.getInstanceNode());
142+
return m;
143+
}
144+
145+
private String getPointer(JsonNodePath evaluationPath) {
146+
if (evaluationPath == null || evaluationPath.getNameCount() == 0) {
147+
return "";
148+
}
149+
150+
StringBuilder sb = new StringBuilder();
151+
for (int i = 0; i < evaluationPath.getNameCount(); i++) {
152+
sb.append('/');
153+
String part = evaluationPath.getName(i);
154+
155+
// escape according to RFC 6901
156+
part = part.replace("~", "~0").replace("/", "~1");
157+
158+
sb.append(part);
159+
}
160+
return sb.toString();
161+
}
162+
163+
@Override
164+
public long getValid() {
165+
return valid.get();
166+
}
167+
168+
@Override
169+
public long getInvalid() {
170+
return invalid.get();
171+
}
172+
173+
@Override
174+
public String getErrorTitle() {
175+
return "JSON validation failed";
176+
}
177+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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.networknt.schema.*;
18+
import com.networknt.schema.resource.*;
19+
import com.predic8.membrane.core.resolver.*;
20+
21+
public class MembraneSchemaLoader implements SchemaLoader {
22+
23+
Resolver resolver;
24+
25+
public MembraneSchemaLoader(Resolver resolver) {
26+
this.resolver = resolver;
27+
}
28+
29+
@Override
30+
public InputStreamSource getSchema(AbsoluteIri absoluteIri) {
31+
32+
33+
System.out.println("absoluteIri = " + absoluteIri);
34+
System.out.println("absoluteIri = " + absoluteIri.toString());
35+
36+
return () -> resolver.resolve(absoluteIri.toString());
37+
38+
39+
}
40+
}

core/src/test/java/com/predic8/membrane/core/interceptor/schemavalidation/JSONSchemaValidationTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ void inValid1() throws Exception {
7979
assertEquals("https://membrane-api.io/problems/user/validation",jn.get("type").textValue());
8080
assertEquals(1, jn.get("errors").size());
8181

82-
// System.out.println("exc.getResponse().getBodyAsStringDecoded() = " + exc.getResponse().getBodyAsStringDecoded());
82+
System.out.println("exc.getResponse().getBodyAsStringDecoded() = " + exc.getResponse().getBodyAsStringDecoded());
8383
}
8484

8585
@Test

0 commit comments

Comments
 (0)