Skip to content

Commit e39272a

Browse files
authored
Add OpenAPI JsonMetaSchema (#1011)
1 parent 48ca3c2 commit e39272a

File tree

19 files changed

+680
-7
lines changed

19 files changed

+680
-7
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
This is a Java implementation of the [JSON Schema Core Draft v4, v6, v7, v2019-09 and v2020-12](http://json-schema.org/latest/json-schema-core.html) specification for JSON schema validation. This implementation supports [Customizing Meta-Schemas, Vocabularies, Keywords and Formats](doc/custom-meta-schema.md).
1515

16-
In addition, it also works for OpenAPI 3.0 request/response validation with some [configuration flags](doc/config.md). For users who want to collect information from a JSON node based on the schema, the [walkers](doc/walkers.md) can help. The JSON parser used is the [Jackson](https://github.com/FasterXML/jackson) parser. As it is a key component in our [light-4j](https://github.com/networknt/light-4j) microservices framework to validate request/response against OpenAPI specification for [light-rest-4j](http://www.networknt.com/style/light-rest-4j/) and RPC schema for [light-hybrid-4j](http://www.networknt.com/style/light-hybrid-4j/) at runtime, performance is the most important aspect in the design.
16+
In addition, [OpenAPI](doc/openapi.md) 3 request/response validation is supported with the use of the appropriate meta-schema. For users who want to collect information from a JSON node based on the schema, the [walkers](doc/walkers.md) can help. The JSON parser used is the [Jackson](https://github.com/FasterXML/jackson) parser. As it is a key component in our [light-4j](https://github.com/networknt/light-4j) microservices framework to validate request/response against OpenAPI specification for [light-rest-4j](http://www.networknt.com/style/light-rest-4j/) and RPC schema for [light-hybrid-4j](http://www.networknt.com/style/light-hybrid-4j/) at runtime, performance is the most important aspect in the design.
1717

1818
## JSON Schema Specification compatibility
1919

@@ -493,6 +493,8 @@ This does not mean that using a schema with a later draft specification will aut
493493

494494
## [Customizing Meta-Schemas, Vocabularies, Keywords and Formats](doc/custom-meta-schema.md)
495495

496+
## [OpenAPI Specification](doc/openapi.md)
497+
496498
## [Validators](doc/validators.md)
497499

498500
## [Configuration](doc/config.md)

doc/openapi.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# OpenAPI Specification
2+
3+
The library includes support for the [OpenAPI Specification](https://swagger.io/specification/).
4+
5+
## Validating a request / response defined in an OpenAPI document
6+
7+
The library can be used to validate requests and responses with the use of the appropriate meta-schema.
8+
9+
| Dialect | Meta-schema |
10+
|--------------------------------------------------|----------------------------------------------------|
11+
| `https://spec.openapis.org/oas/3.0/dialect` | `com.networknt.schema.oas.OpenApi30.getInstance()` |
12+
| `https://spec.openapis.org/oas/3.1/dialect/base` | `com.networknt.schema.oas.OpenApi31.getInstance()` |
13+
14+
```java
15+
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012,
16+
builder -> builder.metaSchema(OpenApi31.getInstance())
17+
.defaultMetaSchemaIri(OpenApi31.getInstance().getIri()));
18+
JsonSchema schema = factory.getSchema(SchemaLocation.of(
19+
"classpath:schema/oas/3.1/petstore.yaml#/components/schemas/PetRequest"));
20+
String input = "{\r\n"
21+
+ " \"petType\": \"dog\",\r\n"
22+
+ " \"bark\": \"woof\"\r\n"
23+
+ "}";
24+
Set<ValidationMessage> messages = schema.validate(input, InputFormat.JSON);
25+
```
26+
27+
## Validating an OpenAPI document
28+
29+
The library can be used to validate OpenAPI documents, however the OpenAPI meta-schema documents are not bundled with the library.
30+
31+
It is recommended that the relevant meta-schema documents are placed in the classpath and are mapped otherwise they will be loaded over the internet.
32+
33+
The following are the documents required to validate a OpenAPI 3.1 document
34+
* `https://spec.openapis.org/oas/3.1/schema-base/2022-10-07`
35+
* `https://spec.openapis.org/oas/3.1/schema/2022-10-07`
36+
* `https://spec.openapis.org/oas/3.1/dialect/base`
37+
* `https://spec.openapis.org/oas/3.1/meta/base`
38+
39+
```java
40+
SchemaValidatorsConfig config = new SchemaValidatorsConfig();
41+
config.setPathType(PathType.JSON_POINTER);
42+
JsonSchema schema = JsonSchemaFactory
43+
.getInstance(VersionFlag.V202012,
44+
builder -> builder.schemaMappers(schemaMappers -> schemaMappers
45+
.mapPrefix("https://spec.openapis.org/oas/3.1", "classpath:oas/3.1")))
46+
.getSchema(SchemaLocation.of("https://spec.openapis.org/oas/3.1/schema-base/2022-10-07"), config);
47+
Set<ValidationMessage> messages = schema.validate(openApiDocument, InputFormat.JSON);
48+
```

src/main/java/com/networknt/schema/DefaultJsonMetaSchemaFactory.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,11 @@ public JsonMetaSchema getMetaSchema(String iri, JsonSchemaFactory schemaFactory,
3939
protected JsonMetaSchema loadMetaSchema(String iri, JsonSchemaFactory schemaFactory,
4040
SchemaValidatorsConfig config) {
4141
try {
42-
return loadMetaSchemaBuilder(iri, schemaFactory, config).build();
42+
JsonMetaSchema result = loadMetaSchemaBuilder(iri, schemaFactory, config).build();
43+
if (result.getKeywords().containsKey("discriminator")) {
44+
config.setOpenAPI3StyleDiscriminators(true);
45+
}
46+
return result;
4347
} catch (InvalidSchemaException e) {
4448
throw e;
4549
} catch (Exception e) {

src/main/java/com/networknt/schema/JsonSchemaFactory.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,12 @@ protected ValidationContext createValidationContext(final JsonNode schemaNode, S
359359
private JsonMetaSchema getMetaSchema(final JsonNode schemaNode, SchemaValidatorsConfig config) {
360360
final JsonNode iriNode = schemaNode.get("$schema");
361361
if (iriNode != null && iriNode.isTextual()) {
362-
return metaSchemas.computeIfAbsent(normalizeMetaSchemaUri(iriNode.textValue()), id -> loadMetaSchema(id, config));
362+
JsonMetaSchema result = metaSchemas.computeIfAbsent(normalizeMetaSchemaUri(iriNode.textValue()),
363+
id -> loadMetaSchema(id, config));
364+
if (result.getKeywords().containsKey("discriminator")) {
365+
config.setOpenAPI3StyleDiscriminators(true);
366+
}
367+
return result;
363368
}
364369
return null;
365370
}
@@ -382,7 +387,11 @@ private JsonMetaSchema getMetaSchemaOrDefault(final JsonNode schemaNode, SchemaV
382387
*/
383388
public JsonMetaSchema getMetaSchema(String iri, SchemaValidatorsConfig config) {
384389
String key = normalizeMetaSchemaUri(iri);
385-
return metaSchemas.computeIfAbsent(key, id -> loadMetaSchema(id, config));
390+
JsonMetaSchema result = metaSchemas.computeIfAbsent(key, id -> loadMetaSchema(id, config));
391+
if (result.getKeywords().containsKey("discriminator")) {
392+
config.setOpenAPI3StyleDiscriminators(true);
393+
}
394+
return result;
386395
}
387396

388397
/**

src/main/java/com/networknt/schema/SchemaId.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,14 @@ public class SchemaId {
4343
* Draft 2020-12.
4444
*/
4545
public static final String V202012 = "https://json-schema.org/draft/2020-12/schema";
46+
47+
/**
48+
* OpenAPI 3.0.
49+
*/
50+
public static final String OPENAPI_3_0 = "https://spec.openapis.org/oas/3.0/dialect";
51+
52+
/**
53+
* OpenAPI 3.1
54+
*/
55+
public static final String OPENAPI_3_1 = "https://spec.openapis.org/oas/3.1/dialect/base";
4656
}

src/main/java/com/networknt/schema/Vocabularies.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ public class Vocabularies {
4242
mapping.put(Vocabulary.V202012_FORMAT_ASSERTION.getIri(), Vocabulary.V202012_FORMAT_ASSERTION);
4343
mapping.put(Vocabulary.V202012_CONTENT.getIri(), Vocabulary.V202012_CONTENT);
4444

45+
mapping.put(Vocabulary.OPENAPI_3_1_BASE.getIri(), Vocabulary.OPENAPI_3_1_BASE);
46+
4547
VALUES = mapping;
4648
}
4749

src/main/java/com/networknt/schema/Vocabulary.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ ValidatorTypeCode.PROPERTYNAMES, ValidatorTypeCode.IF_THEN_ELSE, new NonValidati
9595
"https://json-schema.org/draft/2020-12/vocab/content", new AnnotationKeyword("contentEncoding"),
9696
new AnnotationKeyword("contentMediaType"), new AnnotationKeyword("contentSchema"));
9797

98+
// OpenAPI 3.1
99+
public static final Vocabulary OPENAPI_3_1_BASE = new Vocabulary("https://spec.openapis.org/oas/3.1/vocab/base",
100+
new AnnotationKeyword("example"), ValidatorTypeCode.DISCRIMINATOR, new AnnotationKeyword("externalDocs"),
101+
new AnnotationKeyword("xml"));
102+
98103
private final String iri;
99104
private final Set<Keyword> keywords;
100105

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package com.networknt.schema.oas;
2+
3+
import java.util.Arrays;
4+
5+
import com.networknt.schema.AnnotationKeyword;
6+
import com.networknt.schema.Formats;
7+
import com.networknt.schema.JsonMetaSchema;
8+
import com.networknt.schema.NonValidationKeyword;
9+
import com.networknt.schema.SchemaId;
10+
import com.networknt.schema.SpecVersion;
11+
import com.networknt.schema.ValidatorTypeCode;
12+
13+
/**
14+
* OpenAPI 3.0.
15+
*/
16+
public class OpenApi30 {
17+
private static final String IRI = SchemaId.OPENAPI_3_0;
18+
private static final String ID = "id";
19+
20+
private static class Holder {
21+
private static final JsonMetaSchema INSTANCE;
22+
static {
23+
INSTANCE = JsonMetaSchema.builder(IRI)
24+
.specification(SpecVersion.VersionFlag.V4)
25+
.idKeyword(ID)
26+
.formats(Formats.DEFAULT)
27+
.keywords(Arrays.asList(
28+
new AnnotationKeyword("title"),
29+
ValidatorTypeCode.PATTERN,
30+
ValidatorTypeCode.REQUIRED,
31+
ValidatorTypeCode.ENUM,
32+
ValidatorTypeCode.MINIMUM,
33+
ValidatorTypeCode.MAXIMUM,
34+
ValidatorTypeCode.EXCLUSIVE_MINIMUM,
35+
ValidatorTypeCode.EXCLUSIVE_MAXIMUM,
36+
ValidatorTypeCode.MULTIPLE_OF,
37+
ValidatorTypeCode.MIN_LENGTH,
38+
ValidatorTypeCode.MAX_LENGTH,
39+
ValidatorTypeCode.MIN_ITEMS,
40+
ValidatorTypeCode.MAX_ITEMS,
41+
ValidatorTypeCode.UNIQUE_ITEMS,
42+
ValidatorTypeCode.MIN_PROPERTIES,
43+
ValidatorTypeCode.MAX_PROPERTIES,
44+
45+
ValidatorTypeCode.TYPE,
46+
ValidatorTypeCode.FORMAT,
47+
new AnnotationKeyword("description"),
48+
ValidatorTypeCode.ITEMS,
49+
ValidatorTypeCode.PROPERTIES,
50+
ValidatorTypeCode.ADDITIONAL_PROPERTIES,
51+
new AnnotationKeyword("default"),
52+
ValidatorTypeCode.ALL_OF,
53+
ValidatorTypeCode.ONE_OF,
54+
ValidatorTypeCode.ANY_OF,
55+
ValidatorTypeCode.NOT,
56+
57+
new AnnotationKeyword("deprecated"),
58+
ValidatorTypeCode.DISCRIMINATOR,
59+
new AnnotationKeyword("example"),
60+
new AnnotationKeyword("externalDocs"),
61+
new NonValidationKeyword("nullable"),
62+
ValidatorTypeCode.READ_ONLY,
63+
ValidatorTypeCode.WRITE_ONLY,
64+
new AnnotationKeyword("xml"),
65+
66+
ValidatorTypeCode.REF
67+
))
68+
.build();
69+
}
70+
}
71+
72+
public static JsonMetaSchema getInstance() {
73+
return Holder.INSTANCE;
74+
}
75+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.networknt.schema.oas;
2+
3+
import java.util.Arrays;
4+
import java.util.HashMap;
5+
import java.util.Map;
6+
7+
import com.networknt.schema.Formats;
8+
import com.networknt.schema.JsonMetaSchema;
9+
import com.networknt.schema.NonValidationKeyword;
10+
import com.networknt.schema.SchemaId;
11+
import com.networknt.schema.SpecVersion;
12+
import com.networknt.schema.ValidatorTypeCode;
13+
14+
/**
15+
* OpenAPI 3.1.
16+
*/
17+
public class OpenApi31 {
18+
private static final String IRI = SchemaId.OPENAPI_3_1;
19+
private static final String ID = "$id";
20+
private static final Map<String, Boolean> VOCABULARY;
21+
22+
static {
23+
Map<String, Boolean> vocabulary = new HashMap<>();
24+
vocabulary.put("https://json-schema.org/draft/2020-12/vocab/core", true);
25+
vocabulary.put("https://json-schema.org/draft/2020-12/vocab/applicator", true);
26+
vocabulary.put("https://json-schema.org/draft/2020-12/vocab/unevaluated", true);
27+
vocabulary.put("https://json-schema.org/draft/2020-12/vocab/validation", true);
28+
vocabulary.put("https://json-schema.org/draft/2020-12/vocab/meta-data", true);
29+
vocabulary.put("https://json-schema.org/draft/2020-12/vocab/format-annotation", true);
30+
vocabulary.put("https://json-schema.org/draft/2020-12/vocab/content", true);
31+
vocabulary.put("https://spec.openapis.org/oas/3.1/vocab/base", false);
32+
VOCABULARY = vocabulary;
33+
}
34+
35+
private static class Holder {
36+
private static final JsonMetaSchema INSTANCE;
37+
static {
38+
INSTANCE = JsonMetaSchema.builder(IRI)
39+
.specification(SpecVersion.VersionFlag.V202012)
40+
.idKeyword(ID)
41+
.formats(Formats.DEFAULT)
42+
.keywords(ValidatorTypeCode.getKeywords(SpecVersion.VersionFlag.V202012))
43+
// keywords that may validly exist, but have no validation aspect to them
44+
.keywords(Arrays.asList(
45+
new NonValidationKeyword("definitions")
46+
))
47+
.vocabularies(VOCABULARY)
48+
.build();
49+
}
50+
}
51+
52+
public static JsonMetaSchema getInstance() {
53+
return Holder.INSTANCE;
54+
}
55+
}

src/test/java/com/networknt/schema/MetaSchemaValidationTest.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,14 @@ public class MetaSchemaValidationTest {
3838
*/
3939
@Test
4040
void oas31() throws IOException {
41-
try (InputStream input = MetaSchemaValidationTest.class.getResourceAsStream("/schema/oas/v31/petstore.json")) {
41+
try (InputStream input = MetaSchemaValidationTest.class.getResourceAsStream("/schema/oas/3.1/petstore.json")) {
4242
JsonNode inputData = JsonMapperFactory.getInstance().readTree(input);
4343
SchemaValidatorsConfig config = new SchemaValidatorsConfig();
4444
config.setPathType(PathType.JSON_POINTER);
4545
JsonSchema schema = JsonSchemaFactory
4646
.getInstance(VersionFlag.V202012,
4747
builder -> builder.schemaMappers(schemaMappers -> schemaMappers
48-
.mapPrefix("https://spec.openapis.org/oas/3.1", "classpath:oas/v31")
49-
.mapPrefix("https://json-schema.org", "classpath:")))
48+
.mapPrefix("https://spec.openapis.org/oas/3.1", "classpath:oas/3.1")))
5049
.getSchema(SchemaLocation.of("https://spec.openapis.org/oas/3.1/schema-base/2022-10-07"), config);
5150
Set<ValidationMessage> messages = schema.validate(inputData);
5251
assertEquals(0, messages.size());

0 commit comments

Comments
 (0)