Introdução ao Schematics: um guia para validar dados de um JSON

Garantir regras de negócio é uma questão primordial no mundo da programação. Essas regras também afloram na parte de dados quando precisamos garantir que estamos recebendo as informações necessárias e nos formatos esperados para alimentar nossas bases.

Quando esses dados vem de uma API no formato textual de um JSON, nosso software precisa garantir que o casting desse dado vai trazer formatos consistentes. Existem várias formas/bibliotecas para fazer isso em Python, uma delas é a Schematics.

Schematics: Python Data Structures for Humans™.

Como uma forma similar a frameworks web como Django e bibliotecas de conexão com banco de dados como SQLAlchemy, podemos usar a Schematics para fazer o trabalho pesado de conversão e modelagem dos dados.

Instalando

Como a maioria dos pacotes Python disponíveis, basta instalar usando pip:

$ pip install schematics

Importante: aqui estou usando o Python 3.6.

Material de apoio

Todos os exemplos e códigos que vou mostrar aqui estão disponíveis nesse repositório do GitHub para você utilizar como material de apoio para esse post.


Mão na massa

No mundo de banco de dados relacional, uma tabela possui um conjunto de campos e cada campo tem seu conjunto de regras. Aqui vamos chamar de modelo o equivalente as instruções que definem uma tabela com seus campos e regras.

Vamos supor que nós temos uma tabela chamada Pessoa no nosso banco de dados e, que ela guarda o que vamos chamar de dados de uma Pessoa. Nós vamos querer guardar 3 tipos de informação sobre cada Pessoa, e que esses dados devem seguir, inicialmente, as seguintes regras:

  1. nome: obrigatório, string
  2. idade: inteiro, deve ficar vazio se não possuir dado
  3. linguagens: lista de string, deve ficar vazio se não possuir dado

Também vamos considerar aqui que os dados estão vindo de um JSON e já passaram pelo processo de serem carregados em um dicionário Python. Algo parecido o que mostra esse post do Guia do Mochileiro Python.

Para facilitar a reusabilidade de código vamos usar a seguinte estrutura para escrever nossos modelos:

pessoa/
├── __init__.py
└── models.py

A pasta pessoa vai ser casa dos nossos arquivos Python como o models.py onde vamos escrever nossos modelos.

O Schematics traz um conjunto de estruturas de dados pré-definidas baseadas nos tipos de dados tradicionais como string, int e float e também uma estrutura mais complexa, o Model. Essas estruturas se diferenciam dos tipos padrão pois trazem consigo um conjunto interessante de validações embutidas que vamos nos apoiar daqui para frente.

Para o equivalente a uma tabela vamos definir uma classe que herda de Model dentro do nosso models.py e para os campos vamos usar os tipos que o Schematics já definiu como o StringType, IntType e ListType para string, int e listas respectivamente:

models.py

O parâmetro required=true define os campos obrigatórios dentro do nosso modelo, no nosso caso apenas o campo nome é obrigatório.

A partir daqui conseguimos importar nosso modelo no console Python e utilizá-lo. Vamos testar criando uma instância de Pessoa e ir adicionando dados para teste e para podermos ver se essa instância é válida de acordo com as regras do nosso modelo usando o método embutido .validate():

Exemplo 1: Validação

Você deve ter notado que eu passei uma string para o campo idade, e está se perguntando se isso não causaria uma falha já que o campo idade espera um int. O que acontece é que o Schematics se encarrega também de fazer a conversão automática do dado (casting). Massa né? Mas e se tivéssemos colocado um dado inválido? Por exemplo:

>>> jess.idade = "4p2"

Na hora que fizéssemos a validação veríamos um Traceback pois, o valor string "4p2", não pode ser convertido automaticamente para o int 42 como mostra o erro "Value '42p' is not int":

Exemplo 2: DataError

Outra forma de criar uma instância de pessoa é passando um dicionário já na inicialização:

Exemplo 3: Usando dicionários

Diferentemente do método mostrado no exemplo 1, onde nós rodamos voluntariamente a validação, ao usarmos dicionários para criar a nossa instância de Pessoa, a validação já roda automaticamente na criação do objeto, então você não consegue criar um objeto com valores inválidos.

Exemplo 4: DataError usando dicionários

Serializando

Depois de criar nosso objeto vamos querer exportá-lo seja para devolver esse objeto para um JSON ou para transformar nosso objeto em dicionário Python, vamos ter que serializá-lo. E temos dois métodos muito parecidos para isso:

  • to_native(): serializa nosso objeto para tipos nativos do Python;
  • to_primitive(): serializa nosso objeto para um formato de texto que podemos transformar em JSON.

Outra coisa que precisamos levar em consideração é o que fazer com campos não preenchidos. Colocar no nosso banco um valor None é diferente de um valor vazio. No estado atual se não preenchermos um dos campos teremos um None, veja:

Exemplo 5: Serializando antes

Para corrigir esse detalhe vamos mudar nosso models.py usando um novo parâmetro nos campos não obrigatórios: serialize_when_none=False, vejamos:

models.py

Assim quando preenchemos só parte dos dados e serializamos temos:

Exemplo 6: Serializando depois

Deserializando

JSONs e dicionários Python tem muito em comum. Dentre todas as semelhanças dos dois precisamos lembrar que nenhuma dessas estruturas garantem a ordem dos campos. Por isso o Schematics se apoia na chave do JSON e tenta casar uma chave — e o seu valor — com um campo do nosso modelo.

Com isso, se a chave no dicionário não for a mesma que o nome do campo o Schematics vai levantar um erro de Rogue field:

Exemplo 7: Rogue field

E também podemos nos deparar com um dilema: “Minheus modelos estão em português, mas vou receber dados de uma API onde os campos estão em inglês”. É para esses casos que usamos o parâmetro deserialize_from nos nossos campos. Com ele podemos indicar a qual “novo nome” cada campo irá se corresponder no dado de entrada. Alterando nosso models.py ficamos assim:

models.py

Dessa forma, podemos passar o dicionário com chaves em inglês sem problemas:

Exemplo 8: Deserializando

Reserializando

Mas quando a gente for reserializar o nosso objeto, vamos ter um pequeno probleminha, porque esse passo vai fazer a conversão para campos com nomes iguais aos nomes que definimos nosso modelo que está em português:

Exemplo 9: Reserializando antes

Para resolvermos isso vamos usar o parâmetro serialize_name nos campos do nosso models.py:

models.py

Assim, da mesma forma que conseguimos fazer a deserialização de um dicionário onde os campos não tinham o mesmo nome que as chaves, conseguiremos criar um dicionário com chaves diferentes dos nomes que demos para os campos:

Exemplo 10: Reserializando depois

Essa foi uma introdução ao Schematics. Você pode querer ainda aprofundar os conhecimentos que vimos até aqui e entender como personalizar os modelos e validações, o post “Indo além no Schematics: Personalizando nossos modelos e validações” vai sair jajá e eu volto aqui pra linkar ele ;)


Links