Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add OpenAPI JsonMetaSchema #1011

Merged
merged 1 commit into from
Apr 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

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).

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.
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.

## JSON Schema Specification compatibility

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

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

## [OpenAPI Specification](doc/openapi.md)

## [Validators](doc/validators.md)

## [Configuration](doc/config.md)
Expand Down
48 changes: 48 additions & 0 deletions doc/openapi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# OpenAPI Specification

The library includes support for the [OpenAPI Specification](https://swagger.io/specification/).

## Validating a request / response defined in an OpenAPI document

The library can be used to validate requests and responses with the use of the appropriate meta-schema.

| Dialect | Meta-schema |
|--------------------------------------------------|----------------------------------------------------|
| `https://spec.openapis.org/oas/3.0/dialect` | `com.networknt.schema.oas.OpenApi30.getInstance()` |
| `https://spec.openapis.org/oas/3.1/dialect/base` | `com.networknt.schema.oas.OpenApi31.getInstance()` |

```java
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012,
builder -> builder.metaSchema(OpenApi31.getInstance())
.defaultMetaSchemaIri(OpenApi31.getInstance().getIri()));
JsonSchema schema = factory.getSchema(SchemaLocation.of(
"classpath:schema/oas/3.1/petstore.yaml#/components/schemas/PetRequest"));
String input = "{\r\n"
+ " \"petType\": \"dog\",\r\n"
+ " \"bark\": \"woof\"\r\n"
+ "}";
Set<ValidationMessage> messages = schema.validate(input, InputFormat.JSON);
```

## Validating an OpenAPI document

The library can be used to validate OpenAPI documents, however the OpenAPI meta-schema documents are not bundled with the library.

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.

The following are the documents required to validate a OpenAPI 3.1 document
* `https://spec.openapis.org/oas/3.1/schema-base/2022-10-07`
* `https://spec.openapis.org/oas/3.1/schema/2022-10-07`
* `https://spec.openapis.org/oas/3.1/dialect/base`
* `https://spec.openapis.org/oas/3.1/meta/base`

```java
SchemaValidatorsConfig config = new SchemaValidatorsConfig();
config.setPathType(PathType.JSON_POINTER);
JsonSchema schema = JsonSchemaFactory
.getInstance(VersionFlag.V202012,
builder -> builder.schemaMappers(schemaMappers -> schemaMappers
.mapPrefix("https://spec.openapis.org/oas/3.1", "classpath:oas/3.1")))
.getSchema(SchemaLocation.of("https://spec.openapis.org/oas/3.1/schema-base/2022-10-07"), config);
Set<ValidationMessage> messages = schema.validate(openApiDocument, InputFormat.JSON);
```
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ public JsonMetaSchema getMetaSchema(String iri, JsonSchemaFactory schemaFactory,
protected JsonMetaSchema loadMetaSchema(String iri, JsonSchemaFactory schemaFactory,
SchemaValidatorsConfig config) {
try {
return loadMetaSchemaBuilder(iri, schemaFactory, config).build();
JsonMetaSchema result = loadMetaSchemaBuilder(iri, schemaFactory, config).build();
if (result.getKeywords().containsKey("discriminator")) {
config.setOpenAPI3StyleDiscriminators(true);
}
return result;
} catch (InvalidSchemaException e) {
throw e;
} catch (Exception e) {
Expand Down
13 changes: 11 additions & 2 deletions src/main/java/com/networknt/schema/JsonSchemaFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,12 @@ protected ValidationContext createValidationContext(final JsonNode schemaNode, S
private JsonMetaSchema getMetaSchema(final JsonNode schemaNode, SchemaValidatorsConfig config) {
final JsonNode iriNode = schemaNode.get("$schema");
if (iriNode != null && iriNode.isTextual()) {
return metaSchemas.computeIfAbsent(normalizeMetaSchemaUri(iriNode.textValue()), id -> loadMetaSchema(id, config));
JsonMetaSchema result = metaSchemas.computeIfAbsent(normalizeMetaSchemaUri(iriNode.textValue()),
id -> loadMetaSchema(id, config));
if (result.getKeywords().containsKey("discriminator")) {
config.setOpenAPI3StyleDiscriminators(true);
}
return result;
}
return null;
}
Expand All @@ -382,7 +387,11 @@ private JsonMetaSchema getMetaSchemaOrDefault(final JsonNode schemaNode, SchemaV
*/
public JsonMetaSchema getMetaSchema(String iri, SchemaValidatorsConfig config) {
String key = normalizeMetaSchemaUri(iri);
return metaSchemas.computeIfAbsent(key, id -> loadMetaSchema(id, config));
JsonMetaSchema result = metaSchemas.computeIfAbsent(key, id -> loadMetaSchema(id, config));
if (result.getKeywords().containsKey("discriminator")) {
config.setOpenAPI3StyleDiscriminators(true);
}
return result;
}

/**
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/com/networknt/schema/SchemaId.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,14 @@ public class SchemaId {
* Draft 2020-12.
*/
public static final String V202012 = "https://json-schema.org/draft/2020-12/schema";

/**
* OpenAPI 3.0.
*/
public static final String OPENAPI_3_0 = "https://spec.openapis.org/oas/3.0/dialect";

/**
* OpenAPI 3.1
*/
public static final String OPENAPI_3_1 = "https://spec.openapis.org/oas/3.1/dialect/base";
}
2 changes: 2 additions & 0 deletions src/main/java/com/networknt/schema/Vocabularies.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ public class Vocabularies {
mapping.put(Vocabulary.V202012_FORMAT_ASSERTION.getIri(), Vocabulary.V202012_FORMAT_ASSERTION);
mapping.put(Vocabulary.V202012_CONTENT.getIri(), Vocabulary.V202012_CONTENT);

mapping.put(Vocabulary.OPENAPI_3_1_BASE.getIri(), Vocabulary.OPENAPI_3_1_BASE);

VALUES = mapping;
}

Expand Down
5 changes: 5 additions & 0 deletions src/main/java/com/networknt/schema/Vocabulary.java
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ ValidatorTypeCode.PROPERTYNAMES, ValidatorTypeCode.IF_THEN_ELSE, new NonValidati
"https://json-schema.org/draft/2020-12/vocab/content", new AnnotationKeyword("contentEncoding"),
new AnnotationKeyword("contentMediaType"), new AnnotationKeyword("contentSchema"));

// OpenAPI 3.1
public static final Vocabulary OPENAPI_3_1_BASE = new Vocabulary("https://spec.openapis.org/oas/3.1/vocab/base",
new AnnotationKeyword("example"), ValidatorTypeCode.DISCRIMINATOR, new AnnotationKeyword("externalDocs"),
new AnnotationKeyword("xml"));

private final String iri;
private final Set<Keyword> keywords;

Expand Down
75 changes: 75 additions & 0 deletions src/main/java/com/networknt/schema/oas/OpenApi30.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.networknt.schema.oas;

import java.util.Arrays;

import com.networknt.schema.AnnotationKeyword;
import com.networknt.schema.Formats;
import com.networknt.schema.JsonMetaSchema;
import com.networknt.schema.NonValidationKeyword;
import com.networknt.schema.SchemaId;
import com.networknt.schema.SpecVersion;
import com.networknt.schema.ValidatorTypeCode;

/**
* OpenAPI 3.0.
*/
public class OpenApi30 {
private static final String IRI = SchemaId.OPENAPI_3_0;
private static final String ID = "id";

private static class Holder {
private static final JsonMetaSchema INSTANCE;
static {
INSTANCE = JsonMetaSchema.builder(IRI)
.specification(SpecVersion.VersionFlag.V4)
.idKeyword(ID)
.formats(Formats.DEFAULT)
.keywords(Arrays.asList(
new AnnotationKeyword("title"),
ValidatorTypeCode.PATTERN,
ValidatorTypeCode.REQUIRED,
ValidatorTypeCode.ENUM,
ValidatorTypeCode.MINIMUM,
ValidatorTypeCode.MAXIMUM,
ValidatorTypeCode.EXCLUSIVE_MINIMUM,
ValidatorTypeCode.EXCLUSIVE_MAXIMUM,
ValidatorTypeCode.MULTIPLE_OF,
ValidatorTypeCode.MIN_LENGTH,
ValidatorTypeCode.MAX_LENGTH,
ValidatorTypeCode.MIN_ITEMS,
ValidatorTypeCode.MAX_ITEMS,
ValidatorTypeCode.UNIQUE_ITEMS,
ValidatorTypeCode.MIN_PROPERTIES,
ValidatorTypeCode.MAX_PROPERTIES,

ValidatorTypeCode.TYPE,
ValidatorTypeCode.FORMAT,
new AnnotationKeyword("description"),
ValidatorTypeCode.ITEMS,
ValidatorTypeCode.PROPERTIES,
ValidatorTypeCode.ADDITIONAL_PROPERTIES,
new AnnotationKeyword("default"),
ValidatorTypeCode.ALL_OF,
ValidatorTypeCode.ONE_OF,
ValidatorTypeCode.ANY_OF,
ValidatorTypeCode.NOT,

new AnnotationKeyword("deprecated"),
ValidatorTypeCode.DISCRIMINATOR,
new AnnotationKeyword("example"),
new AnnotationKeyword("externalDocs"),
new NonValidationKeyword("nullable"),
ValidatorTypeCode.READ_ONLY,
ValidatorTypeCode.WRITE_ONLY,
new AnnotationKeyword("xml"),

ValidatorTypeCode.REF
))
.build();
}
}

public static JsonMetaSchema getInstance() {
return Holder.INSTANCE;
}
}
55 changes: 55 additions & 0 deletions src/main/java/com/networknt/schema/oas/OpenApi31.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.networknt.schema.oas;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import com.networknt.schema.Formats;
import com.networknt.schema.JsonMetaSchema;
import com.networknt.schema.NonValidationKeyword;
import com.networknt.schema.SchemaId;
import com.networknt.schema.SpecVersion;
import com.networknt.schema.ValidatorTypeCode;

/**
* OpenAPI 3.1.
*/
public class OpenApi31 {
private static final String IRI = SchemaId.OPENAPI_3_1;
private static final String ID = "$id";
private static final Map<String, Boolean> VOCABULARY;

static {
Map<String, Boolean> vocabulary = new HashMap<>();
vocabulary.put("https://json-schema.org/draft/2020-12/vocab/core", true);
vocabulary.put("https://json-schema.org/draft/2020-12/vocab/applicator", true);
vocabulary.put("https://json-schema.org/draft/2020-12/vocab/unevaluated", true);
vocabulary.put("https://json-schema.org/draft/2020-12/vocab/validation", true);
vocabulary.put("https://json-schema.org/draft/2020-12/vocab/meta-data", true);
vocabulary.put("https://json-schema.org/draft/2020-12/vocab/format-annotation", true);
vocabulary.put("https://json-schema.org/draft/2020-12/vocab/content", true);
vocabulary.put("https://spec.openapis.org/oas/3.1/vocab/base", false);
VOCABULARY = vocabulary;
}

private static class Holder {
private static final JsonMetaSchema INSTANCE;
static {
INSTANCE = JsonMetaSchema.builder(IRI)
.specification(SpecVersion.VersionFlag.V202012)
.idKeyword(ID)
.formats(Formats.DEFAULT)
.keywords(ValidatorTypeCode.getKeywords(SpecVersion.VersionFlag.V202012))
// keywords that may validly exist, but have no validation aspect to them
.keywords(Arrays.asList(
new NonValidationKeyword("definitions")
))
.vocabularies(VOCABULARY)
.build();
}
}

public static JsonMetaSchema getInstance() {
return Holder.INSTANCE;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,14 @@ public class MetaSchemaValidationTest {
*/
@Test
void oas31() throws IOException {
try (InputStream input = MetaSchemaValidationTest.class.getResourceAsStream("/schema/oas/v31/petstore.json")) {
try (InputStream input = MetaSchemaValidationTest.class.getResourceAsStream("/schema/oas/3.1/petstore.json")) {
JsonNode inputData = JsonMapperFactory.getInstance().readTree(input);
SchemaValidatorsConfig config = new SchemaValidatorsConfig();
config.setPathType(PathType.JSON_POINTER);
JsonSchema schema = JsonSchemaFactory
.getInstance(VersionFlag.V202012,
builder -> builder.schemaMappers(schemaMappers -> schemaMappers
.mapPrefix("https://spec.openapis.org/oas/3.1", "classpath:oas/v31")
.mapPrefix("https://json-schema.org", "classpath:")))
.mapPrefix("https://spec.openapis.org/oas/3.1", "classpath:oas/3.1")))
.getSchema(SchemaLocation.of("https://spec.openapis.org/oas/3.1/schema-base/2022-10-07"), config);
Set<ValidationMessage> messages = schema.validate(inputData);
assertEquals(0, messages.size());
Expand Down
67 changes: 67 additions & 0 deletions src/test/java/com/networknt/schema/oas/OpenApi30Test.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright (c) 2024 the original author or authors.
*
* 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.networknt.schema.oas;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import org.junit.jupiter.api.Test;

import com.networknt.schema.DisallowUnknownJsonMetaSchemaFactory;
import com.networknt.schema.InputFormat;
import com.networknt.schema.JsonSchema;
import com.networknt.schema.JsonSchemaFactory;
import com.networknt.schema.SchemaLocation;
import com.networknt.schema.SpecVersion.VersionFlag;
import com.networknt.schema.ValidationMessage;

/**
* OpenApi30Test.
*/
class OpenApi30Test {
/**
* Test with the explicitly configured OpenApi30 instance.
*/
@Test
void validateMetaSchema() {
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V7,
builder -> builder.metaSchema(OpenApi30.getInstance())
.defaultMetaSchemaIri(OpenApi30.getInstance().getIri())
.metaSchemaFactory(DisallowUnknownJsonMetaSchemaFactory.getInstance()));
JsonSchema schema = factory.getSchema(SchemaLocation.of(
"classpath:schema/oas/3.0/petstore.yaml#/paths/~1pet/post/requestBody/content/application~1json/schema"));
String input = "{\r\n"
+ " \"petType\": \"dog\",\r\n"
+ " \"bark\": \"woof\"\r\n"
+ "}";
Set<ValidationMessage> messages = schema.validate(input, InputFormat.JSON);
assertEquals(0, messages.size());

String invalid = "{\r\n"
+ " \"petType\": \"dog\",\r\n"
+ " \"meow\": \"meeeooow\"\r\n"
+ "}";
messages = schema.validate(invalid, InputFormat.JSON);
assertEquals(2, messages.size());
List<ValidationMessage> list = messages.stream().collect(Collectors.toList());
assertEquals("oneOf", list.get(0).getType());
assertEquals("required", list.get(1).getType());
assertEquals("bark", list.get(1).getProperty());
}
}
Loading
Loading