Java example of meta-schema v1

AmazonSPAPI

# Example Validator Implementation for Java

For Java applications, the networknt/json-schema-validator (opens new window) library supports JSON Schema Draft 2019-09 and custom vocabularies. The following example demonstrates how to utilize the networknt/json-schema-validator (opens new window) library to validate payloads with instances of the Amazon Product Type Definition Meta-Schema. There is no requirement to use this specific library or the example implementation. Amazon does not provide technical support for third-party JSON Schema libraries and this is provided as an example only.

# Schema Configuration

When using the networknt/json-schema-validator (opens new window) to validate instances of the Amazon Product Type Definition Meta-Schema with custom vocabulary, the meta-schema is configured as part of the JsonSchemaFactory.

Constants:

// $id of the Amazon Product Type Definition Meta-Schema.
String schemaId = "https://schemas.amazon.com/selling-partners/definitions/product-types/meta-schema/v1";

// Local copy of the Amazon Product Type Definition Meta-Schema.
String metaSchemaPath = "./amazon-product-type-definition-meta-schema-v1.json";

// Local copy of an instance of the Amazon Product Type Definition Meta-Schema.
String luggageSchemaPath = "./luggage.json";

// Keywords that are informational only and do not require validation.
List<String> nonValidatingKeywords = ImmutableList.of("editable", "enumNames");
1
2
3
4
5
6
7
8
9
10
11

Configure Meta-Schema:

// Standard JSON Schema 2019-09 that Amazon Product Type Definition Meta-Schema extends from.
JsonMetaSchema standardMetaSchema = JsonMetaSchema.getV201909();

// Build Amazon Product Type Definition Meta Schema with the standard JSON Schema 2019-09 as the blueprint.
// Register custom keyword validation classes (see below).
JsonMetaSchema metaSchema = JsonMetaSchema.builder(SCHEMA_ID, standardMetaSchema)
    .addKeywords(NON_VALIDATING_KEYWORDS.stream().map(NonValidationKeyword::new)
        .collect(Collectors.toSet()))
    .addKeyword(new MaxUniqueItemsKeyword())
    .addKeyword(new MaxUtf8ByteLengthKeyword())
    .addKeyword(new MinUtf8ByteLengthKeyword())
    .build();
1
2
3
4
5
6
7
8
9
10
11
12

Build JsonSchemaFactory:

// URIFetcher to route meta-schema references to local copy.
URIFetcher uriFetcher = uri -> {
    // Use the local copy of the meta-schema instead of retrieving from the web.
    if (schemaId.equalsIgnoreCase(uri.toString())) {
        return Files.newInputStream(Paths.get(metaSchemaPath));
    }

    // Default to the existing fetcher for other schemas.
    return new URLFetcher().fetch(uri);
};

// Build the JsonSchemaFactory.
JsonSchemaFactory schemaFactory = new JsonSchemaFactory.Builder()
    .defaultMetaSchemaURI(schemaId)
    .addMetaSchema(standardMetaSchema)
    .addMetaSchema(metaSchema)
    .uriFetcher(uriFetcher, "https")
    .build();
    
// Create the JsonSchema instance.
JsonSchema luggageSchema = schemaFactory.getSchema(new String(Files.readAllBytes(Paths.get(luggageSchemaPath))));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# Payload Validation

With an instance of the Amazon Product Type Definition Meta-Schema loaded as a JsonSchema instance, payloads can be validated using the instance.

// Create a JsonNode for the payload (this can be constructed in code, read from a file, etc.).
JsonNode payload = new ObjectMapper().readValue(new File("./payload.json"), JsonNode.class);

// Validate the payload and get any resulting validation messages.
Set<ValidationMessage> messages = luggageSchema.validate(payload);
1
2
3
4
5

If no validation messages are returned, validation passed. Otherwise, inspect the validation messages to identify errors with the payload.

# Keyword Validation

The networknt/json-schema-validator (opens new window) supports validating custom vocabulary by using classes that extend the AbstractKeyword class and provide the validation logic.

See https://github.com/networknt/json-schema-validator/blob/master/doc/validators.md (opens new window).

The following examples illustrate extensions of the AbstractKeyword class that validate the custom vocabulary in instances of the Amazon Product Type Definition Meta-Schema.

# MaxUniqueItemsKeyword Class

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.collect.Streams;
import com.networknt.schema.AbstractJsonValidator;
import com.networknt.schema.AbstractKeyword;
import com.networknt.schema.CustomErrorMessageType;
import com.networknt.schema.JsonSchema;
import com.networknt.schema.JsonValidator;
import com.networknt.schema.ValidationContext;
import com.networknt.schema.ValidationMessage;
import org.apache.commons.lang3.StringUtils;

import java.text.MessageFormat;
import java.util.Iterator;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * Example validator for the "maxUniqueItems" keyword.
 */
public class MaxUniqueItemsKeyword extends AbstractKeyword {

    private static final MessageFormat ERROR_MESSAGE_FORMAT = new MessageFormat("Each combination of selector "
            + "values may only occur {1} times. The following selector value combination occurs too many times: {2}");

    private static final String KEYWORD = "maxUniqueItems";
    private static final String SELECTORS = "selectors";

    public MaxUniqueItemsKeyword() {
        super(KEYWORD);
    }

    @Override
    public JsonValidator newValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema,
            ValidationContext validationContext) {
            
        // Only process if the provided schema value is a number.
        if (!JsonNodeType.NUMBER.equals(schemaNode.getNodeType())) {
            return null;
        }

        int maxUniqueItems = schemaNode.asInt();

        // Get the selector properties configured on the scheme element, if they exist. Otherwise, this validator
        // defaults to using all properties.
        Set<String> selectors = getSelectorProperties(parentSchema);

        return new AbstractJsonValidator(this.getValue()) {
        
            @Override
            public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String at) {
            
                // Only process if the node is an array, as selectors and unique items do not apply to other data
                // types.
                if (node.isArray()) {
                
                    // Create a property-value map of each items properties (selectors) and count the number of
                    // occurrences for each combination.
                    Map<Map<String, String>, Integer> uniqueItemCounts = Maps.newHashMap();
                    
                    node.forEach(instance -> {
                    
                        // Only process instances that are objects.
                        if (instance.isObject()) {
                            Map<String, String> uniqueKeys = Maps.newHashMap();

                            Iterator<Map.Entry<String, JsonNode>> fieldIterator = instance.fields();
                            while (fieldIterator.hasNext()) {
                                Map.Entry<String, JsonNode> entry = fieldIterator.next();
                                // If no selectors are configured, always add. Otherwise only add if the property is
                                // a selector.
                                if (selectors.isEmpty() || selectors.contains(entry.getKey())) {
                                    uniqueKeys.put(entry.getKey(), entry.getValue().asText());
                                }
                            }

                            // Iterate count and put in counts map.
                            int count = uniqueItemCounts.getOrDefault(uniqueKeys, 0) + 1;
                            uniqueItemCounts.put(uniqueKeys, count);
                        }
                    });

                    // Find first selector combination with too many instances.
                    Optional<Map<String, String>> uniqueKeysWithTooManyItems = uniqueItemCounts.entrySet()
                            .stream().filter(entry -> entry.getValue() > maxUniqueItems).map(Map.Entry::getKey)
                            .findFirst();

                    // Return a failed validation if a selector combination has too many instances.
                    if (uniqueKeysWithTooManyItems.isPresent()) {
                        return fail(CustomErrorMessageType.of(KEYWORD, ERROR_MESSAGE_FORMAT), at,
                                Integer.toString(maxUniqueItems), uniqueKeysWithTooManyItems.get().toString());
                    }
                }

                return pass();
            }
        };
    }

    private Set<String> getSelectorProperties(JsonSchema parentSchema) {
        if (parentSchema.getSchemaNode().has(SELECTORS) && parentSchema.getSchemaNode().get(SELECTORS).isArray()) {
            return Streams.stream(parentSchema.getSchemaNode().get(SELECTORS)).map(JsonNode::asText)
                    .filter(StringUtils::isNotBlank).collect(Collectors.toSet());
        }
        return Sets.newHashSet();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111

# MaxUtf8ByteLengthKeyword Class

package com.amazon.spucs.tests.keywords;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import com.networknt.schema.AbstractJsonValidator;
import com.networknt.schema.AbstractKeyword;
import com.networknt.schema.CustomErrorMessageType;
import com.networknt.schema.JsonSchema;
import com.networknt.schema.JsonValidator;
import com.networknt.schema.ValidationContext;
import com.networknt.schema.ValidationMessage;

import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.Set;

/**
 * Example validator for the "maxUtf8ByteLength" keyword.
 */
public class MaxUtf8ByteLengthKeyword  extends AbstractKeyword {

    private static final MessageFormat ERROR_MESSAGE_FORMAT =
            new MessageFormat("Value must be less than or equal {1} bytes in length.");

    private static final String KEYWORD = "maxUtf8ByteLength";

    public MaxUtf8ByteLengthKeyword() {
        super(KEYWORD);
    }

    @Override
    public JsonValidator newValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema,
            ValidationContext validationContext) {
            
        // Only process if the provided schema value is a number.
        if (!JsonNodeType.NUMBER.equals(schemaNode.getNodeType())) {
            return null;
        }

        int maxUtf8ByteLength = schemaNode.asInt();

        return new AbstractJsonValidator(this.getValue()) {
        
            @Override
            public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String at) {
            
                // Get the value as a string and evaluate its length in bytes.
                String value = node.asText();
                if (value.getBytes(StandardCharsets.UTF_8).length > maxUtf8ByteLength) {
                    return fail(CustomErrorMessageType.of(KEYWORD, ERROR_MESSAGE_FORMAT), at,
                            Integer.toString(maxUtf8ByteLength));
                }
                return pass();
            }
        };
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

# MinUtf8ByteLengthKeyword Class

package com.amazon.spucs.tests.keywords;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeType;
import com.networknt.schema.AbstractJsonValidator;
import com.networknt.schema.AbstractKeyword;
import com.networknt.schema.CustomErrorMessageType;
import com.networknt.schema.JsonSchema;
import com.networknt.schema.JsonValidator;
import com.networknt.schema.ValidationContext;
import com.networknt.schema.ValidationMessage;

import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.Set;

/**
 * Example validator for the "minUtf8ByteLength" keyword.
 */
public class MinUtf8ByteLengthKeyword extends AbstractKeyword {

    private static final MessageFormat ERROR_MESSAGE_FORMAT =
            new MessageFormat("Value must be greater than or equal {1} bytes in length.");

    private static final String KEYWORD = "minUtf8ByteLength";

    public MinUtf8ByteLengthKeyword() {
        super(KEYWORD);
    }

    @Override
    public JsonValidator newValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema,
            ValidationContext validationContext) {
        // Only process if the provided schema value is a number.
        if (!JsonNodeType.NUMBER.equals(schemaNode.getNodeType())) {
            return null;
        }

        int minUtf8ByteLength = schemaNode.asInt();

        return new AbstractJsonValidator(this.getValue()) {
        
            @Override
            public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String at) {
            
                // Get the value as a string and evaluate its length in bytes.
                String value = node.asText();
                if (value.getBytes(StandardCharsets.UTF_8).length < minUtf8ByteLength) {
                    return fail(CustomErrorMessageType.of(KEYWORD, ERROR_MESSAGE_FORMAT), at,
                            Integer.toString(minUtf8ByteLength));
                }
                return pass();
            }
        };
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56