Por: @mariliars
Publicado em: 2018-11-14

Estrutura das Entidades e Validadores

Com a implementação da estrutura clean arquiteture a equipe de desenvolvimento observou repetição de código para a declaração das entidades e seus validadores. Além disso, também existem problemas com a estrutura de validação. Entre esses problemas estão:

  • Validação de estruturas de objetos embutidos, ou seja, um objeto com relacionamento entre outros objetos
  • Existe a dificuldade na validação de valores nulos ou de strings vazias
  • Não existe a conversão automática dos dados

Diante desses problemas, foi elaborado um estudo sobre formas de estruturar a declaração das entidades e como validar seus campos. Os critérios a serem observados neste estudo são:

  • Dados em formato de classes/structs
  • Validação embutida nos dados
  • Conversão automática de tipos
  • Conversão para formato json
  • Valores padrões para os campos

As bibliotecas estudadas são listadas a seguir e são detalhadas ao longo deste post.

Além dessas bibliotecas, em pesquisas relacionadas foram encontradas as bibliotecas de validação de dados: Schema e Kim. Para estas, não foram elaborados protótipos. Mais informações no site oficial.

Os exemplos de implementação das bibliotecas aqui mencionadas estão disponíveis no repositório python-fields-validators.

Attr

A biblioteca attr permite escrever classes com definição de tipos de dados, validação e transformação de dados. Algumas das características da biblioteca são:

  • Permite tipagem dos campos
  • Permite conversão de tipo dos campos
  • Permite adicionar validadores customizados
  • Consegue validar dados de subobjetos quando adicionado um validador customizado
  • Define valores default

Exemplo de uso

from attr import attrs, Factory, ib, asdict, filters, fields, validators
from typing import List

def validate_str(instance, attribute, value):
    if type(value) != str:
        raise ValueError("Must be a string")


def validate_list_int(instance, attribute, value):
    for item in value:
        if type(item) != int:
            raise ValueError("Must be a list of integer")

def validate_list_roman_numbers(instance, attribute, value):
    if type(value) != list:
        raise ValueError("Must be a list of RomanNumber")
    for item in value:
        if type(item) != RomanNumber:
            raise ValueError("Must be a list of RomanNumber")

@attrs
class RomanNumber:
    number = ib(type=str, validator=validators.instance_of(str))

@attrs
class AlphabetNumbers(object):
    a = ib(type=int, default=42)
    b = ib(default=Factory(list))
    c = ib(factory=list)
    d = ib()
    e = ib(factory=dict)
    f = ib(default=42,convert=int)
    g = ib(default=42, init=False)
    h = ib(factory=str, validator=validate_str)
    i = ib(factory=int, validator=validators.instance_of(int))  
    list_number: List[int] = ib(default=Factory(List[int]), validator=validators.and_(validators.instance_of(List), validate_list_int))
    list_roman_number: List[RomanNumber] = ib(default=Factory(List[RomanNumber]), validator=validators.and_(validators.instance_of(List), validate_list_roman_numbers))
    roman_number: RomanNumber = ib(default=None, validator=validators.instance_of(RomanNumber))
    secret_key = ib(factory=str, repr=False)

    def __attrs_post_init__(self):
        self.g = 50

    @d.default
    def _factory_d(self):
        return {}

    def to_dict(self):
        return asdict(self)

Schematics

Schematics permite escrever classes com definição de tipos de dados, validação e transformação de dados. Características da biblioteca:

  • É possível instanciar o objeto vazio ou com um dicionário
  • Campos string aceitam int
  • Campos int aceitam números escritos utilizando string
  • O valor default dos campos é sempre nulo
  • Permite personalizar mensagens
  • Permite definir nomes de serialização e deserealização
  • Permite adicionar validadores customizados
  • Gera mock dos objetos
  • Suporte a idiomas
  • Permite converter os dados

FormEncode

FormEncode é um pacote de validação e geração de formulários. Algumas de suas características são:

  • Validação dos campos

  • Permite adicionar validadores customizados

  • Cosegue validar regex

  • Validadores nativos:

  • if_missing: para setar um valor default

  • not_empty: torna o campo requerido

  • strip: para campos do tipo string

  • Outros: if_empty, if_invalid, if_invalid_python, accept_python

Obs: Pouco documentado

Pydantic

Pydantic é uma bibliocteca de validação de dados utilizando o pytdantic BaseModel ou dataclass. Algumas de suas características são:

  • As validações são escritas como funções da classe utilizando o decorator @validator, informando quais campos irão aplicar a validação.
  • Possui formas de priorizar as validações, executando uma validação antes da outra utilizando o pre
  • Possui validação de objetos com o whole
  • Permite criar schemas
  • Permite criar tipos de dados customizados
  • Possui estrutura de mensagens de erro
  • Caso não seja definida uma estrutura de validação converte números escritos no formato strnig para inteiro e vice versa

Exemplo de uso

import json
from typing import List

from pydantic import BaseModel, ValidationError, validator


class StrictStr(str):
    @classmethod
    def get_validators(cls):
        yield cls.validate

    @classmethod
    def validate(cls, v):
        if not isinstance(v, str):
            raise ValueError(f'strict string: str expected not {type(v)}')
        return v

class OtherDemo(BaseModel):
    field1 : str = None
    field2 : int = None
    field3: List[int] = []

class DemoModel(BaseModel):
    numbers: List[int] = []
    people: List[str] = []
    other_demo: OtherDemo = None
    list_demo: List[OtherDemo] = []
    field_str_custom: StrictStr = None

    @validator('numbers', 'people', 'list_demo', whole=True)
    def check_list(cls, v):
        if not isinstance(v, list):
            raise ValueError('Must be int')
        return v

    @validator('numbers', pre=True)
    def check_int(cls, v):
        if not isinstance(v, int):
            raise ValueError('Must be int')
        return v

    @validator('people', pre=True)
    def check_str(cls, v):
        if not isinstance(v, str):
            raise ValueError('Must be str')
        return v

    @validator('list_demo', 'other_demo', pre=True)
    def check_demo(cls, v):
        if not isinstance(v, OtherDemo):
            raise ValueError('Must be OtherDemo')
        return v

    @validator('numbers')
    def check_numbers_low(cls, v):
        if v > 4:
            raise ValueError(f'number too large {v} > 4')
        return v

    @validator('numbers', whole=True)
    def check_sum_numbers_low(cls, v):
        if sum(v) > 8:
            raise ValueError(f'sum of numbers greater than 8')
        return v

Colander

Colander é uma biblioteca de serialização e desserialização de dados. Algumas de suas características são:

  • Definição de schemas de dados em forma de classes
  • Permite criar tipos de dados customizados, bem como suas validações
  • Consegue validar objetos aninhados
  • Converte números escritos no formato string para inteiro e vice versa

Exemplo de uso

from colander import TupleSchema, SchemaNode, Int, String, Range, OneOf, MappingSchema, SequenceSchema, Invalid

class RangedInt(SchemaNode):
    schema_type = Int
    default = 10
    title = 'Ranged Int'

    def validator(self, node, cstruct):
        if not 0 < cstruct < 10:
            raise Invalid(node, 'Must be between 0 and 10')

class Friend(TupleSchema):
    rank = SchemaNode(Int(), validator=Range(0, 10))
    name = SchemaNode(String())

class Phone(MappingSchema):
    location = SchemaNode(String(), validator=OneOf(['home', 'work']))
    number = SchemaNode(String())

class Friends(SequenceSchema):
    friend = Friend()

class Phones(SequenceSchema):
    phone = Phone()

class Person(MappingSchema):
    name = SchemaNode(String())
    age = RangedInt()
    friends = Friends()
    phones = Phones(missing=None)

Referências