Exemplo em Java de Meta-Schema (v1)

Um exemplo em Java para validar payloads com instâncias do Meta-Schema de definição de tipo de produto da Amazon.

Exemplo de implementação do validador para Java

Para aplicativos Java, a biblioteca networknt/json-schema-validator é compatível com JSON Schema Draft 2019-09 e vocabulários personalizados. O exemplo a seguir demonstra como utilizar a biblioteca networknt/json-schema-validator para validar cargas com instâncias do Meta-Schema de definição de tipo de produto da Amazon. Não há necessidade de usar essa biblioteca específica ou a implementação de exemplo. A Amazon não fornece suporte técnico para bibliotecas de esquema JSON de terceiros, isso é fornecido apenas como exemplo.

Configuração do Schema

Ao usar o networknt/json-schema-validator para validar instâncias do Meta-Schema de definição de tipo de produto da Amazon com vocabulário personalizado, o Meta-Schema é configurado como parte do JsonSchemaFactory.

Constantes:

// $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");

Configurar 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();

Construir 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))));

Validação do Payload

Com uma instância do Meta-Schema de definição de tipo de produto da Amazon carregada como uma instância JsonSchema, as cargas úteis podem ser validadas usando a instância.

// 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);

Se nenhuma mensagem de validação for retornada, a validação foi aprovada. Caso contrário, inspecione as mensagens de validação para identificar erros com a carga útil.

Validação de palavra-chave

O networknt/json-schema-validator suporta a validação de vocabulário personalizado usando classes que estendem a classe AbstractKeyword e fornecem a lógica de validação.

Consulte https://github.com/networknt/json-schema-validator/blob/master/doc/validators.md.

Os exemplos a seguir ilustram extensões da classe AbstractKeyword que validam o vocabulário personalizado em instâncias do Meta-Schema de definição de tipo de produto da Amazon.

Classe MaxUniqueItemsKeyword

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();
    }
}

Classe MaxUtf8ByteLengthKeyword

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();
            }
        };
    }
}

Classe MinUtf8ByteLengthKeyword

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();
            }
        };
    }
}