Utilizando JSON Schema para validações

Guilherme Schiavone
Tradeback
Published in
9 min readNov 5, 2021
Fonte: https://training.cherriz.de/cherriz-training/1.0.0/xml_und_co/json_schema.html

Atualmente, é um processo fundamental garantirmos a consistência dos dados no fluxo de execução de nossas aplicações e podemos encontrar diferentes maneiras de realizar essa tarefa, como por exemplo, na stack .NET com C# podemos utilizar o conceitos de Design By Contracts e Notification Pattern onde definimos um contrato que deve ser cumprido pelos modelos de input e são produzidas notifications de saída para caso alguma validação seja esbarrada.

Entretanto, hoje venho propor um outro caminho que podemos seguir nos processos de validação de nossas aplicações chamado JSON Schema.

Entendendo o JSON Schema

O JSON Schema funciona como um vocabulário ou padrão que nos permite definir um modelo de validação e anotação que nossos documentos JSON devem seguir. Algo próximo de um schema que definimos em nossos bancos de dados relacionais, onde especificamos colunas, tamanhos e tipos específicos para as tabelas. Nesse padrão, através de Validation Keywords conseguimos especificar keys e o range possível de valores que nossas propriedades JSON podem assumir. Nesse primeiro momento pode parecer confuso, mas vou esclarecer com um exemplo:

Imagine que esteja trabalhando com uma Web API e tenha um body request formado pelos dados de identificação de uma empresa, sendo eles: “CompanyId” (Identificador único da empresa), “CompanyName” (Nome da empresa) e “CustomerIds” (Array de ids de identificação de clientes para qual a respectiva empresa presta serviços — algo como uma chave estrangeira).

Modelo de exemplo do case apresentado.

Se trabalharmos com a stack .NET, poderíamos como comentado anteriormente elaborar um contrato para as validações desses campos, utilizando bibliotecas como o Flunt ou o Fluent Validations. Mas se partirmos para a abordagem do JSON Schema, teríamos que primeiro pensar na elaboração do schema para esse documento de company.

Elaborando o schema, teríamos algo como isso:

  • $schema: Representa um draft do padrão schema que está sendo usado como base, normalmente esse campo é utilizado para versionamento.
  • $uri: Representa uma URI que funciona como identificador único onde o schema possa ser acessado pelas aplicações que o vão consumir.
  • type: Consiste em uma validation keyword, ou seja, uma chave que servirá no processo de validação. Com essa chave, podemos especificar o tipo desse componente raiz, que na maioria dos casos vai ser representado por um object, mas se estivermos esperando receber um array, podemos apontar o type como sendo um array.
  • description: Representa um caráter documentacional do schema, onde podemos especificar de forma resumida o que esse schema representa.
  • title: Outro campo documentacional, onde podemos especificar um title para o schema e normalmente apontamos para o componente, no caso company, que é representado.
  • properties: Quando o componente raiz é um object, podemos entrar mais a fundo e especificar um formato e possíveis valores para as props que esse objeto irá possuir. No caso, temos companyId, companyName e customerIds. Para cada prop, podemos usar uma série de validation keywords, que validam comprimentos, formatos usando regex patterns, datas e etc. Para mais detalhes de quais são os tipos possíveis (consiste nos tipos primitivos do JS) ou quais são os tipos de validation keywords que podem ser usadas para cada tipo, acesse essa referência do JsonSchema:
    https://json-schema.org/understanding-json-schema/reference/ .
  • required: Representa quais são as properties obrigatórias do objeto, caso o type do componente raiz seja object. No nosso case, os campos obrigatórios são o companyId e o customerIds.

O JSON Schema de forma geral apresenta uma serie de benefícios, sendo eles:

  • Facilidade na criação de testes unitários para validação.
  • Possibilita um clear human reading, facilitando a leitura tanto para indivíduos técnicos como não técnicos.
  • Uma descrição mais categorizada dos dados existentes.

A grande pergunta que pode surgir agora é como podemos fazer para implementar esse schema em uma aplicação real.

Não se preocupe, na verdade é bem simples e diversos módulos implementados em diversas tecnologias já abstraem isso para nós. Como exemplo, hoje utilizarei a implementação desse schema como validador no .NET e no Python que são lugares comuns para utilizá-los quando estamos trabalhando com Web API´s representadas pelo Django no Python e pelo ASP.NET Core no .NET. Mas é claro, qualquer caso de uso em que seja necessário a validação de um layout JSON é cabido a utilização do JsonSchema.

Para uma referência completa dos diferentes módulos que implementam esse padrão nas diferentes linguagens, acesse: https://json-schema.org/implementations.html

Implementando no .NET

  • Inicialmente, vamos criar uma Console Application para utilizarmos como POC desse padrão:

Podemos trabalhar com dois exemplos na implementação desse padrão nessa tecnologia, um exemplo mais simples utilizando um código mais direto e um exemplo mais robusto que imagino que seria aplicado em um cenário real com uma modelagem de classes mais interessante utilizando herança, encapsulamento, composição e SOLID.

Antes de tudo, precisamos definir qual módulo vamos utilizar para implementar o JSON Schema. Atualmente, temos dois para o .NET seguindo a documentação:

Vou optar pelo primeiro, pois é mais popular, além de ser implementado pela Newtonsoft que também implementa o famoso serializer JSON.

No Package Manager:

Install-Package Newtonsoft.Json.Schema

  • Para o exemplo simples, implementei uma classe que chamei de JsonSchemaSimpleExample
  • Nessa classe, implementei alguns métodos, mas como diria Jack, O Estripador, vamos por partes.
  • Primeiramente, implementei um método responsável por definir o schema hardcoded, nele especifico um formato JSON e dou um parse usando o método estático da classe JSchema. Essa tipo é basicamente a representação do schema em memória.
Schema definido conforme case apresentado no inicio do artigo.
  • Em seguida, defino dois métodos para tratar a validação de forma divergente. O primeiro apenas valida o objeto passado conforme o Schema e retorna um booleano. O segundo utiliza uma estrutura de Try-Catch, pois a implementação do extension method Validate() do JObject da Newtonsoft lança uma exception com a mensagem de validação do problema encontrado no JSON passado, e queremos tratar para printar para o usuário. Ambos utilizam a classe JObject que consiste basicamente na representação encapsulada e serializada do objeto que recebemos por parâmetro.
  • Ambos os métodos utilizam a invocação do GetCustomizedJsonSerializer(), que é responsável por definir um serializador personalizado que utiliza no contractResolver uma estratégia de nomeação de props baseada em camelCase:

Isso é importante, pois se definirmos o schema com as props em camelCase, é fundamental definirmos a serialização para camelCase também, pois caso não, como o schema é case sensitive, não haverá o match das props.

  • Em seguida, utilizamos essa implementação na classe Program e testamos seus comportamentos:
Utilizando a classe JsonSchemaSimpleExample para testar os diferentes comportamentos do módulo Json Schema.
  • Olhando atentamente, é possível notar que testamos dois modelos de representação de empresa diferentes utilizando objetos anônimos, uma válida que tem todas as props corretas e uma inválida que não tem a prop obrigatória customerIds.
  • Executando, temos o seguinte resultado:

Tendo mostrado o exemplo simples, podemos partir para um exemplo mais complexo e robusto.

  • Inicialmente, implementei uma classe que chamei de BaseSchema<T>. Pensei esse modelo baseado no conceito de composição:
  • Nela definimos algumas props que irão compor o modelo, além de um tipo T que representa as properties desse schema. Se analisarmos bem, podemos notar que temos aqui as mesmas props definidas pelo padrão e essa classe é baseada no type object do schema. Além disso, defini dois Attributes, sendo um na prop Id e outro na Schema, para que quando fossem serializadas recebessem o nome correto segundo o padrão.
  • Essa classe servirá para montarmos qualquer estrutura de schema encapsulada por tipos que desejarmos.
  • Em seguida, defini outra classe que representará o modelo tipado do nosso schema de company e nesta fiz uma composição de outras duas classes que representarão as props que compõe esse schema. No caso, para simplificar o exemplo utilizei somente companyId e customerIds que são as props obrigatórias do schema original.
CompanySchema formado pelos tipos complexos que representam cada tipo
Ambas classes que representam as properties do objeto implementam uma interface ISchemaProperties que torna obrigatório no contrato a implementação da Description e do Type
  • Agora, podemos testar o exemplo mais robusto. Para isso, implementei uma classe estática que sabe validar os Schemas e Objects. Chamei ela de SchemaValidator:
SchemaValidator se aproxima do que fiz no exemplo simples.
  • Um detalhe importante é que no serializador customizado que serializa o Schema, defini a prop DefaultValueHandling como IgnoreAndPopulate para ele não serializar as partes do Schema que não tenham sido populadas.
  • Agora, podemos testar o exemplo complexo finalmente:
  • Como resultado, temos:

Vale ressaltar que esse segundo exemplo consiste em uma proposta de implementação mais robusta que elaborei, mas nada impede que novos modelos de implementação surjam a partir desse conceito, sempre pensando em Escalabilidade, Expressividade, Manutenabilidade e nas melhores práticas do SOLID.

Implementando no Python

Para o Python, optei pelo módulo jsonschema, por se tratar de uma implementação mais simples, mas nada impede que outros módulos sejam selecionados.

  • Primeiramente, podemos instalar o módulo utilizando o PyPi:

pip install jsonschema

  • Em seguida, podemos importar a função validate do módulo instalado:
  • Agora, definimos um dictionary que representa o nosso schema e chamamos a função validate(), o passando como parâmetro. Vale ressaltar que aqui a função se comporta de forma semelhante com o que aconteceu no C#, pois é gerada uma exception cuja a mensagem é a de validação:
  • Como resultado, temos:

Atualmente, em aplicações corporativas é muito comum seguirmos modelos arquiteturais robustos baseados em domínio, como é o caso do DDD (Domain Driven Design), Arquitetura Hexagonal ou Clean Architecture. Nesse ponto, entra uma discussão sobre qual a melhor forma de aplicarmos o JSON Schema e acredito que aqui caiamos em um campo mais interpretativo, já que surge uma importante pergunta: As validações devem ser feitas pelo schema ou pelas entidades de domínio?

Na minha visão, podemos sim utilizar o Json Schema no processo de validação, mas sendo implementado em uma camada anterior a do domínio, isso é, o JSON Schema teria uma camada segregada para a realização das validações iniciais do JSON que foi serializado a partir do request, se encaixando bem em uma camada intermediária de sanitização e validação.

Entretanto, o core business, ou seja, as regras de negócio de mais alto nível que exigirem cálculos ou validações de fluxos específicos deveriam ser mantidas no nível do domínio, com suas entidades garantindo a consistência dos dados modelados, seus value objects e seus aggregates. Uma outra possibilidade, seria essa camada de validação com JSON Schema ser parte do domínio, tendo suas validações integradas as entidades.

Fica o apontamento de que os módulos que implementam o JSON Schema também possuem uma feature que possibilita a geração de schemas dinâmicamente a partir de modelos estabelecidos, além de permitir que schemas referenciem uns aos outros, mas isso fica para um próximo artigo.

No geral, espero que tenha gostado e entendido. Caso tenha algo mais a acrescentar, por favor, adicione um comentário pois eu ficaria muito feliz em participar de um tópico de discussão.

Espero te ver no próximo artigo! Até a próxima!

Referências:

Link para o repositório com a POC:

--

--

Guilherme Schiavone
Tradeback

Amante da tecnologia, de games, filmes trash e desenvolvedor. O que você pode me ensinar hoje?