xref: /aosp_15_r20/external/json-schema-validator/doc/custom-meta-schema.md (revision 78c4dd6aa35290980cdcd1623a7e337e8d021c7c)
1*78c4dd6aSAndroid Build Coastguard Worker# Customizing Meta-Schemas, Vocabularies, Keywords and Formats
2*78c4dd6aSAndroid Build Coastguard Worker
3*78c4dd6aSAndroid Build Coastguard WorkerThe meta-schemas, vocabularies, keywords and formats can be customized with appropriate configuration of the `JsonSchemaFactory` that is used to create instances of `JsonSchema`.
4*78c4dd6aSAndroid Build Coastguard Worker
5*78c4dd6aSAndroid Build Coastguard Worker## Creating a custom keyword
6*78c4dd6aSAndroid Build Coastguard Worker
7*78c4dd6aSAndroid Build Coastguard WorkerA custom keyword can be implemented by implementing the `com.networknt.schema.Keyword` interface.
8*78c4dd6aSAndroid Build Coastguard Worker
9*78c4dd6aSAndroid Build Coastguard Worker```java
10*78c4dd6aSAndroid Build Coastguard Workerpublic class EqualsKeyword implements Keyword {
11*78c4dd6aSAndroid Build Coastguard Worker    @Override
12*78c4dd6aSAndroid Build Coastguard Worker    public String getValue() {
13*78c4dd6aSAndroid Build Coastguard Worker        return "equals";
14*78c4dd6aSAndroid Build Coastguard Worker    }
15*78c4dd6aSAndroid Build Coastguard Worker    @Override
16*78c4dd6aSAndroid Build Coastguard Worker    public JsonValidator newValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath,
17*78c4dd6aSAndroid Build Coastguard Worker            JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext)
18*78c4dd6aSAndroid Build Coastguard Worker            throws JsonSchemaException, Exception {
19*78c4dd6aSAndroid Build Coastguard Worker        return new EqualsValidator(schemaLocation, evaluationPath, schemaNode, parentSchema, this, validationContext, false);
20*78c4dd6aSAndroid Build Coastguard Worker    }
21*78c4dd6aSAndroid Build Coastguard Worker}
22*78c4dd6aSAndroid Build Coastguard Worker```
23*78c4dd6aSAndroid Build Coastguard Worker
24*78c4dd6aSAndroid Build Coastguard Worker```java
25*78c4dd6aSAndroid Build Coastguard Workerpublic class EqualsValidator extends BaseJsonValidator {
26*78c4dd6aSAndroid Build Coastguard Worker    private static ErrorMessageType ERROR_MESSAGE_TYPE = new ErrorMessageType() {
27*78c4dd6aSAndroid Build Coastguard Worker        @Override
28*78c4dd6aSAndroid Build Coastguard Worker        public String getErrorCode() {
29*78c4dd6aSAndroid Build Coastguard Worker            return "equals";
30*78c4dd6aSAndroid Build Coastguard Worker        }
31*78c4dd6aSAndroid Build Coastguard Worker    };
32*78c4dd6aSAndroid Build Coastguard Worker
33*78c4dd6aSAndroid Build Coastguard Worker    private final String value;
34*78c4dd6aSAndroid Build Coastguard Worker    public EqualsValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode,
35*78c4dd6aSAndroid Build Coastguard Worker            JsonSchema parentSchema, Keyword keyword,
36*78c4dd6aSAndroid Build Coastguard Worker            ValidationContext validationContext, boolean suppressSubSchemaRetrieval) {
37*78c4dd6aSAndroid Build Coastguard Worker        super(schemaLocation, evaluationPath, schemaNode, parentSchema, ERROR_MESSAGE_TYPE, keyword, validationContext,
38*78c4dd6aSAndroid Build Coastguard Worker                suppressSubSchemaRetrieval);
39*78c4dd6aSAndroid Build Coastguard Worker        this.value = schemaNode.textValue();
40*78c4dd6aSAndroid Build Coastguard Worker    }
41*78c4dd6aSAndroid Build Coastguard Worker    @Override
42*78c4dd6aSAndroid Build Coastguard Worker    public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode,
43*78c4dd6aSAndroid Build Coastguard Worker            JsonNodePath instanceLocation) {
44*78c4dd6aSAndroid Build Coastguard Worker        if (!node.asText().equals(value)) {
45*78c4dd6aSAndroid Build Coastguard Worker            return Collections
46*78c4dd6aSAndroid Build Coastguard Worker                    .singleton(message().message("{0}: must be equal to ''{1}''")
47*78c4dd6aSAndroid Build Coastguard Worker                            .arguments(value)
48*78c4dd6aSAndroid Build Coastguard Worker                            .instanceLocation(instanceLocation).instanceNode(node).build());
49*78c4dd6aSAndroid Build Coastguard Worker        };
50*78c4dd6aSAndroid Build Coastguard Worker        return Collections.emptySet();
51*78c4dd6aSAndroid Build Coastguard Worker    }
52*78c4dd6aSAndroid Build Coastguard Worker}
53*78c4dd6aSAndroid Build Coastguard Worker```
54*78c4dd6aSAndroid Build Coastguard Worker
55*78c4dd6aSAndroid Build Coastguard Worker## Adding a keyword to a standard dialect
56*78c4dd6aSAndroid Build Coastguard Worker
57*78c4dd6aSAndroid Build Coastguard WorkerA custom keyword can be added to a standard dialect by customizing its meta-schema which is identified by its IRI.
58*78c4dd6aSAndroid Build Coastguard Worker
59*78c4dd6aSAndroid Build Coastguard WorkerThe following adds a custom keyword to the Draft 2020-12 dialect.
60*78c4dd6aSAndroid Build Coastguard Worker
61*78c4dd6aSAndroid Build Coastguard Worker```java
62*78c4dd6aSAndroid Build Coastguard WorkerJsonMetaSchema metaSchema = JsonMetaSchema.builder(JsonMetaSchema.getV202012())
63*78c4dd6aSAndroid Build Coastguard Worker        .keyword(new EqualsKeyword())
64*78c4dd6aSAndroid Build Coastguard Worker        .build();
65*78c4dd6aSAndroid Build Coastguard WorkerJsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012, builder -> builder.metaSchema(metaSchema));
66*78c4dd6aSAndroid Build Coastguard Worker```
67*78c4dd6aSAndroid Build Coastguard Worker
68*78c4dd6aSAndroid Build Coastguard Worker## Creating a custom meta-schema
69*78c4dd6aSAndroid Build Coastguard Worker
70*78c4dd6aSAndroid Build Coastguard WorkerA custom meta-schema can be created by using a standard dialect as a base.
71*78c4dd6aSAndroid Build Coastguard Worker
72*78c4dd6aSAndroid Build Coastguard WorkerThe following creates a custom meta-schema `https://www.example.com/schema` with a custom keyword using the Draft 2020-12 dialect as a base.
73*78c4dd6aSAndroid Build Coastguard Worker
74*78c4dd6aSAndroid Build Coastguard Worker```java
75*78c4dd6aSAndroid Build Coastguard WorkerJsonMetaSchema dialect = JsonMetaSchema.getV202012();
76*78c4dd6aSAndroid Build Coastguard WorkerJsonMetaSchema metaSchema = JsonMetaSchema.builder("https://www.example.com/schema", dialect)
77*78c4dd6aSAndroid Build Coastguard Worker        .keyword(new EqualsKeyword())
78*78c4dd6aSAndroid Build Coastguard Worker        .build();
79*78c4dd6aSAndroid Build Coastguard WorkerJsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012, builder -> builder.metaSchema(metaSchema));
80*78c4dd6aSAndroid Build Coastguard Worker```
81*78c4dd6aSAndroid Build Coastguard Worker
82*78c4dd6aSAndroid Build Coastguard Worker## Associating vocabularies to a dialect
83*78c4dd6aSAndroid Build Coastguard Worker
84*78c4dd6aSAndroid Build Coastguard WorkerCustom vocabularies can be associated with a particular dialect by configuring a `com.networknt.schema.VocabularyFactory` on its meta-schema.
85*78c4dd6aSAndroid Build Coastguard Worker
86*78c4dd6aSAndroid Build Coastguard Worker```java
87*78c4dd6aSAndroid Build Coastguard WorkerVocabularyFactory vocabularyFactory = iri -> {
88*78c4dd6aSAndroid Build Coastguard Worker    if ("https://www.example.com/vocab/equals".equals(iri)) {
89*78c4dd6aSAndroid Build Coastguard Worker        return new Vocabulary("https://www.example.com/vocab/equals", new EqualsKeyword());
90*78c4dd6aSAndroid Build Coastguard Worker    }
91*78c4dd6aSAndroid Build Coastguard Worker    return null;
92*78c4dd6aSAndroid Build Coastguard Worker};
93*78c4dd6aSAndroid Build Coastguard WorkerJsonMetaSchema metaSchema = JsonMetaSchema.builder(JsonMetaSchema.getV202012())
94*78c4dd6aSAndroid Build Coastguard Worker        .vocabularyFactory(vocabularyFactory)
95*78c4dd6aSAndroid Build Coastguard Worker        .build();
96*78c4dd6aSAndroid Build Coastguard WorkerJsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012, builder -> builder.metaSchema(metaSchema));
97*78c4dd6aSAndroid Build Coastguard Worker```
98*78c4dd6aSAndroid Build Coastguard Worker
99*78c4dd6aSAndroid Build Coastguard WorkerThe following custom meta-schema `https://www.example.com/schema` will use the custom vocabulary `https://www.example.com/vocab/equals`.
100*78c4dd6aSAndroid Build Coastguard Worker
101*78c4dd6aSAndroid Build Coastguard Worker```json
102*78c4dd6aSAndroid Build Coastguard Worker{
103*78c4dd6aSAndroid Build Coastguard Worker  "$schema": "https://json-schema.org/draft/2020-12/schema",
104*78c4dd6aSAndroid Build Coastguard Worker  "$id": "https://www.example.com/schema",
105*78c4dd6aSAndroid Build Coastguard Worker  "$vocabulary": {
106*78c4dd6aSAndroid Build Coastguard Worker    "https://www.example.com/vocab/equals": true,
107*78c4dd6aSAndroid Build Coastguard Worker    "https://json-schema.org/draft/2020-12/vocab/applicator": true,
108*78c4dd6aSAndroid Build Coastguard Worker    "https://json-schema.org/draft/2020-12/vocab/core": true
109*78c4dd6aSAndroid Build Coastguard Worker  },
110*78c4dd6aSAndroid Build Coastguard Worker  "allOf": [
111*78c4dd6aSAndroid Build Coastguard Worker    { "$ref": "https://json-schema.org/draft/2020-12/meta/applicator" },
112*78c4dd6aSAndroid Build Coastguard Worker    { "$ref": "https://json-schema.org/draft/2020-12/meta/core" }
113*78c4dd6aSAndroid Build Coastguard Worker  ]
114*78c4dd6aSAndroid Build Coastguard Worker}
115*78c4dd6aSAndroid Build Coastguard Worker```
116*78c4dd6aSAndroid Build Coastguard Worker
117*78c4dd6aSAndroid Build Coastguard WorkerNote that `"https://www.example.com/vocab/equals": true` means that if the vocabulary is unknown the meta-schema will fail to successfully load while `"https://www.example.com/vocab/equals": false` means that an unknown vocabulary will still successfully load.
118*78c4dd6aSAndroid Build Coastguard Worker
119*78c4dd6aSAndroid Build Coastguard Worker## Unknown keywords
120*78c4dd6aSAndroid Build Coastguard Worker
121*78c4dd6aSAndroid Build Coastguard WorkerBy default unknown keywords are treated as annotations. This can be customized by configuring a `com.networknt.schema.KeywordFactory` on its meta-schema.
122*78c4dd6aSAndroid Build Coastguard Worker
123*78c4dd6aSAndroid Build Coastguard WorkerThe following configuration will cause a `InvalidSchemaException` to be thrown if an unknown keyword is used.
124*78c4dd6aSAndroid Build Coastguard Worker
125*78c4dd6aSAndroid Build Coastguard Worker```java
126*78c4dd6aSAndroid Build Coastguard WorkerJsonMetaSchema metaSchema = JsonMetaSchema.builder(JsonMetaSchema.getV202012())
127*78c4dd6aSAndroid Build Coastguard Worker        .unknownKeywordFactory(DisallowUnknownKeywordFactory.getInstance())
128*78c4dd6aSAndroid Build Coastguard Worker        .build();
129*78c4dd6aSAndroid Build Coastguard WorkerJsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012, builder -> builder.metaSchema(metaSchema));
130*78c4dd6aSAndroid Build Coastguard Worker```
131*78c4dd6aSAndroid Build Coastguard Worker
132*78c4dd6aSAndroid Build Coastguard Worker## Creating a custom format
133*78c4dd6aSAndroid Build Coastguard Worker
134*78c4dd6aSAndroid Build Coastguard WorkerA custom format can be implemented by implementing the `com.networknt.schema.Format` interface.
135*78c4dd6aSAndroid Build Coastguard Worker
136*78c4dd6aSAndroid Build Coastguard Worker```java
137*78c4dd6aSAndroid Build Coastguard Workerpublic class MatchNumberFormat implements Format {
138*78c4dd6aSAndroid Build Coastguard Worker    private final BigDecimal compare;
139*78c4dd6aSAndroid Build Coastguard Worker
140*78c4dd6aSAndroid Build Coastguard Worker    public MatchNumberFormat(BigDecimal compare) {
141*78c4dd6aSAndroid Build Coastguard Worker        this.compare = compare;
142*78c4dd6aSAndroid Build Coastguard Worker    }
143*78c4dd6aSAndroid Build Coastguard Worker    @Override
144*78c4dd6aSAndroid Build Coastguard Worker    public boolean matches(ExecutionContext executionContext, ValidationContext validationContext, JsonNode value) {
145*78c4dd6aSAndroid Build Coastguard Worker        JsonType nodeType = TypeFactory.getValueNodeType(value, validationContext.getConfig());
146*78c4dd6aSAndroid Build Coastguard Worker        if (nodeType != JsonType.NUMBER && nodeType != JsonType.INTEGER) {
147*78c4dd6aSAndroid Build Coastguard Worker            return true;
148*78c4dd6aSAndroid Build Coastguard Worker        }
149*78c4dd6aSAndroid Build Coastguard Worker        BigDecimal number = value.isBigDecimal() ? value.decimalValue() : BigDecimal.valueOf(value.doubleValue());
150*78c4dd6aSAndroid Build Coastguard Worker        number = new BigDecimal(number.toPlainString());
151*78c4dd6aSAndroid Build Coastguard Worker        return number.compareTo(compare) == 0;
152*78c4dd6aSAndroid Build Coastguard Worker    }
153*78c4dd6aSAndroid Build Coastguard Worker    @Override
154*78c4dd6aSAndroid Build Coastguard Worker    public String getName() {
155*78c4dd6aSAndroid Build Coastguard Worker        return "matchnumber";
156*78c4dd6aSAndroid Build Coastguard Worker    }
157*78c4dd6aSAndroid Build Coastguard Worker}
158*78c4dd6aSAndroid Build Coastguard Worker```
159*78c4dd6aSAndroid Build Coastguard Worker
160*78c4dd6aSAndroid Build Coastguard Worker## Adding a format to a standard dialect
161*78c4dd6aSAndroid Build Coastguard Worker
162*78c4dd6aSAndroid Build Coastguard WorkerA custom format can be added to a standard dialect by customizing its meta-schema which is identified by its IRI.
163*78c4dd6aSAndroid Build Coastguard Worker
164*78c4dd6aSAndroid Build Coastguard WorkerThe following adds a custom format to the Draft 2020-12 dialect.
165*78c4dd6aSAndroid Build Coastguard Worker
166*78c4dd6aSAndroid Build Coastguard Worker```java
167*78c4dd6aSAndroid Build Coastguard WorkerJsonMetaSchema metaSchema = JsonMetaSchema.builder(JsonMetaSchema.getV202012())
168*78c4dd6aSAndroid Build Coastguard Worker        .format(new MatchNumberFormat(new BigDecimal("12345")))
169*78c4dd6aSAndroid Build Coastguard Worker        .build();
170*78c4dd6aSAndroid Build Coastguard WorkerJsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012, builder -> builder.metaSchema(metaSchema));
171*78c4dd6aSAndroid Build Coastguard Worker```
172*78c4dd6aSAndroid Build Coastguard Worker
173*78c4dd6aSAndroid Build Coastguard Worker## Customizing the format keyword
174*78c4dd6aSAndroid Build Coastguard Worker
175*78c4dd6aSAndroid Build Coastguard WorkerThe format keyword implementation to use can be customized by supplying a `FormatKeywordFactory` to the meta-schema that creates an instance of the subclass of `FormatKeyword`.
176*78c4dd6aSAndroid Build Coastguard Worker
177*78c4dd6aSAndroid Build Coastguard Worker```java
178*78c4dd6aSAndroid Build Coastguard WorkerJsonMetaSchema metaSchema = JsonMetaSchema.builder(JsonMetaSchema.getV202012())
179*78c4dd6aSAndroid Build Coastguard Worker        .formatKeywordFactory(CustomFormatKeyword::new)
180*78c4dd6aSAndroid Build Coastguard Worker        .build();
181*78c4dd6aSAndroid Build Coastguard WorkerJsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012, builder -> builder.metaSchema(metaSchema));
182*78c4dd6aSAndroid Build Coastguard Worker```
183*78c4dd6aSAndroid Build Coastguard Worker
184*78c4dd6aSAndroid Build Coastguard Worker## Unknown formats
185*78c4dd6aSAndroid Build Coastguard Worker
186*78c4dd6aSAndroid Build Coastguard WorkerBy default unknown formats are ignored unless the format assertion vocabulary is used for that meta-schema. Note that the format annotation vocabulary with the configuration to enable format assertions is not equivalent to the format assertion vocabulary.
187*78c4dd6aSAndroid Build Coastguard Worker
188*78c4dd6aSAndroid Build Coastguard WorkerTo ensure that errors are raised when unknown formats are used, the `SchemaValidatorsConfig` can be configured to set `format` as strict.
189*78c4dd6aSAndroid Build Coastguard Worker
190*78c4dd6aSAndroid Build Coastguard Worker
191*78c4dd6aSAndroid Build Coastguard Worker## Loading meta-schemas
192*78c4dd6aSAndroid Build Coastguard Worker
193*78c4dd6aSAndroid Build Coastguard WorkerBy default meta-schemas that aren't explicitly configured in the `JsonSchemaFactory` will be automatically loaded.
194*78c4dd6aSAndroid Build Coastguard Worker
195*78c4dd6aSAndroid Build Coastguard WorkerThis means that the following `JsonSchemaFactory` will still be able to process `$schema` with other dialects such as Draft 7 or Draft 2019-09 as the meta-schemas for those dialects will be automatically loaded. This will also attempt to load custom meta-schemas with custom vocabularies. Draft 2020-12 will be used by default if `$schema` is not defined.
196*78c4dd6aSAndroid Build Coastguard Worker
197*78c4dd6aSAndroid Build Coastguard Worker```java
198*78c4dd6aSAndroid Build Coastguard WorkerJsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012);
199*78c4dd6aSAndroid Build Coastguard Worker```
200*78c4dd6aSAndroid Build Coastguard Worker
201*78c4dd6aSAndroid Build Coastguard WorkerIf this is undesirable, for instance to restrict the meta-schemas used only to those explicitly configured in the `JsonSchemaFactory` a `com.networknt.schema.JsonMetaSchemaFactory` can be configured.
202*78c4dd6aSAndroid Build Coastguard Worker
203*78c4dd6aSAndroid Build Coastguard Worker```java
204*78c4dd6aSAndroid Build Coastguard WorkerJsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012,
205*78c4dd6aSAndroid Build Coastguard Worker        builder -> builder.metaSchemaFactory(DisallowUnknownJsonMetaSchemaFactory.getInstance()));
206*78c4dd6aSAndroid Build Coastguard Worker```
207