go-validator

module
v0.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 28, 2023 License: MIT

README

Validação de DTO

A validação de DTO tem um processo mais automatizado e simples, para evitar duplicação de código e para facilitar o processo de identificação de informações incorretas. Para isso, utilizamos um Validator Helper, que auxilia a identificar os erros via tags de atributos nos DTOs.

Problema

Primeiro, vamos assumir que temos um DTO chamado Account, considerando a seguinte estrutura:

type Account struct {
	Name      string `json:"name"`
	BirthDate string `json:"birth_date"`
	Email     string `json:"email"`
}

Em cada atributo informamos que há uma tag chamada json cujo valor é um nome do atributo no padrão snake_case. Quando temos um conjunto de dados (ex.: um Map/JSON) que será encaixado nessa estrutura, além de ser colocado cada valor no seu devido lugar, temos que também realizar um processo de validação de valores, como no nome, email, data de nascimento e nos demais atributos. De forma manual, adicionaríamos várias linhas de código para podermos validar cada atributo (considerando também que teríamos vários lugares do código utilizando a mesma lógica ou outras estruturas com atributos semelhantes).

Nesse caso, teríamos o seguinte código considerando a estrutura fornecida:

func main() {
	timeLayout := "2006-01-02"
	data := map[string]interface{}{
		"name":       "Test Man",
		"birth_date": "2000-01-01",
	}
	serialized := request.Account{}
	serialized.Name = data["name"].(string) // might fail
	serialized.BirthDate = data["birth_date"].(string) // might fail
	if serialized.Name == "" {
		log.Fatal("Name is required!")
	} else if len(strings.Split(serialized.Name, ",")) == 1 {
		log.Fatal("You need to provide the first and the last name!")
	} else if v, err := time.Parse(timeLayout, serialized.BirthDate); err != nil {
		log.Fatal("Invalid date format!")
	} else if v.After(time.Now()) {
		log.Fatal("Invalid birth date!")
	}
}

No método acima, estamos verificando alguns valores atribuídos na variável serialized. Porém com o tempo teremos mais estruturas com atributos parecidos e vários lugares utilizando da mesma validação. Além disso, temos que pensar na tipagem the cada valor passado no map data (como colocado quando atribuindo os atributos Name e BirthDate).

Solução

O Validator Helper foi criado para cobrir esses casos faltantes e conseguirmos ter uma menor quantidade de linhas para resolver problemas de validação de conjunto de dados. Considerando cada atributo, por padrão, como opcional, é possível informar quais atributos são obrigatórios e/ou têm validações específicas.

Para utilizarmos, precisamos utilizar o seguinte trecho de código:

...
	dto, err := validator.ValidateDTO[Account](data)
	fmt.Println("DTO:", dto)
	fmt.Println("ERROR:", err)
...

O método ValidateDTO vai utilizar um tipo genêrico para basear a conversão de valores presentes dentro dos dados da variável data. Essa função retorna o dto convertido e um erro do tipo ValidationError. Caso os dados não sejam válidos e existam erros relacionados, a variável dto será nil e terá um erro. Caso contrário, a variável dto irá conter os valores esperados. Neste caso, temos nome e data de nascimento:

# go run main.go
DTO: &{Test Man 2000-01-01 }
ERROR: <nil>

Nesse ponto, não informamos nenhuma validação específica e o validador conseguiu definir os atributos Name e BirthDate. Porém, se informarmos valores errados considerando a tipagem da estrutura, o seguinte irá acontecer:

...
	data := map[string]interface{}{
		"name":       "Test Man",
		"birth_date": 2,
	}
...

Executando:

# go run main.go
DTO: &{Test Man  }
ERROR: <nil>

O valor da data de nascimento não foi definido no atributo BirthDate e nenhum erro foi retornado. Isso aconteceu pois a variável é facultativa e nenhuma validação foi requisitada. Para consertarmos isso e definirmos a data de nascimento como obrigatória, precisamos adicionar uma tag chamada validate de validação na estrutura:

type Account struct {
	Name      string `json:"name"`
	BirthDate string `json:"birth_date" validate:"required"`
	Email     string `json:"email"`
}

Com isso, ao executar o mesmo código, o atributo BirthDate será considerado obrigatório e um erro será adquirido:

# go run main.go
DTO: <nil>
ERROR: 'birth_date' field type must be 'string'

Caso você queira que o mesmo atributo ainda seja opcional, mas quando informado (seja diferente de nil) seja convertido devidamente pelo tipo informado, basta adicionar a validação de tipo na tag validate:

type Account struct {
	Name      string `json:"name"`
	BirthDate string `json:"birth_date" validate:"type"`
	Email     string `json:"email"`
}

Ao executar o mesmo código, teremos a seguinte saída:

# go run main.go
DTO: <nil>
ERROR: 'birth_date' field type must be 'string'

Validação Parcial

Até então vimos que é possível ter uma validação bruta, onde caso der certo, teremos os dados. Caso contrário, teremos o erro. Em alguns casos talvez seja necessário ter ambos, reaproveitando os valores que a validação foi feita e o valor se encontra correto. Para isso, basta utilizarmos o método ValidateDTOPartially, onde a validação, por mais que nos retorne um erro, o DTO será retornado com os dados validados até então.

Para testarmos isso, considerando que temos a seguinte estrutura e dados:

type Account struct {
	Name      string `json:"name" validate:"required"`
	BirthDate string `json:"birth_date" validate:"type"`
	Email     string `json:"email"`
}
...
	data := map[string]interface{}{
		"name":       "",
		"birth_date": "01/01/2000",
	}
...

A saída esperada será:

# go run main.go
DTO: &{ 01/01/2000 }
ERROR: 'name' field of type 'string' is missing or empty

Validação de Tipos Específicos

Data

No exemplo anterior vimos que é possível adicionar verificações obrigatórias ou não que validam sua presença ou seu tipo quando valores de um atributo específico estão presentes. Porém, dependendo do atributo, necessitamos de uma validação a mais. Um desses casos é o caso do atributo BirthDate que precisa de uma validação de data.

Para isso, podemos alterar a nossa estrutura para ter o seguinte valor na tag validate:

type Account struct {
	Name      string `json:"name"`
	BirthDate string `json:"birth_date" validate:"type,date"`
	Email     string `json:"email"`
}

Com isso, utilizando o seguinte map de dados:

...
	data := map[string]interface{}{
		"name":       "Test Man",
		"birth_date": "2000",
	}
...

Essa é a saída esperada:

# go run main.go
DTO: <nil>
ERROR: 'birth_date' field doesn't match with the '2006-01-02' format

Por padrão, o Validator Helper utiliza o formato 2006-01-02 para validar datas. Quando informando uma data correta (ex.: "2000-01-01"), a saída esperada é:

# go run main.go
DTO: &{Test Man 2000-01-01 }
ERROR: <nil>

Se você deseja definir um formato específico de validação de data, basta informar na tag validate seguindo a sintaxe "date=<format>", onde "<format>" pode ser "2006" caso queira somente o ano, "2006-01" caso queira o ano e o mês ou não definir nenhum formato (definindo somente "date") para pegar toda a formatação de data. Confira a saída esperada quando na tag validate está definido "type,date=2006-01" e passamos a data completa ("2000-01-01"):

# go run main.go
DTO: <nil>
ERROR: 'birth_date' field doesn't match with the '2006-01' format

E agora quando informamos a data no formato correto ("2000-01"):

# go run main.go
DTO: &{Test Man 2000-01 }
ERROR: <nil>

Essa definição de formatação livre de data permite com que seja possível, por exemplo, informar datas no padrão brasileiro como "01/01/2000" definindo na tag validate a sintaxe "date=02/01/2006":

# go run main.go
DTO: <nil>
ERROR: 'birth_date' field doesn't match with the '02/01/2006' format
Email

Para validação de valores do tipo email, basta definir a regra email dentro da tag validate da seguinte forma:

type Account struct {
	Name      string `json:"name"`
	BirthDate string `json:"birth_date" validate:"type,date=02/01/2006"`
	Email     string `json:"email" validate:"email"`
}

Com isso, caso informemos o seguinte valor em map:

...
	data := map[string]interface{}{
		"name":       "Test Man",
		"birth_date": "01/01/2000",
		"email":      "Test",
	}
...

A saída esperada será:

# go run main.go
DTO: <nil>
ERROR: the value provided for the 'email' field isn't a valid email
Quantidade de Caracteres

Podemos validar a quantidade de caracteres pelas regras minlen=<something>, len=<something> e maxlen=<something>, onde no lugar de <something> podemos colocar um valor que pode determinar o mínimo, específico ou máximo tamanho de um texto. Ambas as regras "minlen" e "maxlen" são inclusivas (minlen funciona semanticamente como "a partir de X" e maxlen funciona semanticamente como "até X").

minlen

Para utilizar a largura mínima, iremos adicionar a regra minlen no atributo Name, ficando da seguinte forma:

type Account struct {
	Name string `json:"name" validate:"minlen=5"`
}

Ao fornecer os seguintes dados:

...
	data := map[string]interface{}{
		"name": "Test",
	}
...

Essa será a saída esperada:

# go run main.go
DTO: <nil>
ERROR: 'name' field must have at least 5 characters

len

Para utilizar a largura específica, iremos adicionar a regra len no atributo Name, ficando da seguinte forma:

type Account struct {
	Name string `json:"name" validate:"len=5"`
}

Ao fornecer os seguintes dados:

...
	data := map[string]interface{}{
		"name": "Test",
	}
...

Essa será a saída esperada:

# go run main.go
DTO: <nil>
ERROR: 'name' field must have 5 characters

maxlen

Para utilizar a largura máxima, iremos adicionar a regra maxlen no atributo Name, ficando da seguinte forma:

type Account struct {
	Name string `json:"name" validate:"len=10"`
}

Ao fornecer os seguintes dados:

...
	data := map[string]interface{}{
		"name": "My Awesome and Beautiful Name",
	}
...

Essa será a saída esperada:

# go run main.go
DTO: <nil>
ERROR: 'name' field must have 5 characters at max

Slices

Para validar slices, você pode utilizar as regras slice:len=X, slice:minlen=X e slice:maxlen=X, onde X é a quantidade desejada.

Exemplo: quando é necessário cadastrar uma lista de usuários e necessitamos da lista do nome das pessoas, nós normalmente utilizaríamos a seguinte estrutura:

type Account struct {
	Names []string `json:"names"`
}

E os seguintes valores:

...
	data := map[string]interface{}{
		"names": []string{
			"test",
			"test",
		},
	}
...

Ao executarmos sem nenhuma regra de Slice, o resultado esperado será:

# go run main.go
DTO: &{[test test]}
ERROR: <nil>

Com isso, podemos explorar as regras de validação de quantidade de elementos (veja abaixo).

len

Para utilizar a regra de quantidade específica, iremos adicionar a regra slice:len no atributo Names, ficando da seguinte forma:

type Accounts struct {
	Names []string `json:"name" validate:"slice:len=2"`
}

Ao fornecer os seguintes dados:

...
  data := map[string]interface{}{
  	"names": []string{
  		"Test",
  	},
  }
...

Essa será a saída esperada:

# go run main.go
DTO: <nil>
ERROR: the 'names' field must have 2 elements

minlen

Para utilizar a regra de quantidade mínima, iremos adicionar a regra slice:minlen no atributo Names, ficando da seguinte forma:

type Accounts struct {
	Names []string `json:"name" validate:"slice:minlen=2"`
}

Ao fornecer os seguintes dados:

...
  data := map[string]interface{}{
  	"names": []string{
  		"Test",
  	},
  }
...

Essa será a saída esperada:

# go run main.go
DTO: <nil>
ERROR: the 'names' field must have at least 2 elements

maxlen

Para utilizar a regra de quantidade máxima, iremos adicionar a regra slice:maxlen no atributo Names, ficando da seguinte forma:

type Accounts struct {
	Names []string `json:"name" validate:"slice:maxlen=2"`
}

Ao fornecer os seguintes dados:

...
  data := map[string]interface{}{
  	"names": []string{
  		"Test1",
  		"Test2",
  		"Test3",
  	},
  }
...

Essa será a saída esperada:

# go run main.go
DTO: <nil>
ERROR: the 'names' field must have 2 elements at max

Validações de elementos

Caso queira colocar uma validação específica em cada elemento da lista, é possível somente informando o tipo de validação no atributo, assim todas as validações que não sejam de lista serão aplicadas em cada elemento.

Exemplo: ao invés de nomes, vamos pensar que é necessário adquirir uma lista de emails onde precisamos validar se cada item fornecido é um email válido. Nesse caso, teremos a seguinte estrutura:

type Accounts struct {
	Emails []string `json:"emails" validate:"email"`
}

Considerando que estamos fornecendo a seguinte lista de emails:

...
	data := map[string]interface{}{
		"emails": []string{
			"Test1",
			"test@email.com",
			"Test3",
		},
	}
...

Essa é a resposta esperada:

# go run main.go
DTO: <nil>
ERROR: the value provided for the 'emails[0]' field isn't a valid email & the value provided for the 'emails[2]' field isn't a valid email
Objetos Aninhados

Também é possível fazer a validação de objetos aninhados. Supondo um exemplo onde temos que passar todas as informações de perfil de uma conta, podemos utilizar a seguinte estrutura:

type Profile struct {
	FirstName string `json:"firstName" validate:"required"`
	LastName  string `json:"lastName"`
	Email     string `json:"email" validate:"required,email"`
}

type Account struct {
	Profile Profile `json:"profile"`
}

Nessa, existem regras de validação somente nos atributos dos atributos da estrutura Profile que está aninhada na estrutura Account.

Podemos utilizar o seguinte código para validar a estrutura:

func main() {
	data := map[string]interface{}{}
	dto, err := validator.ValidateDTO[Account](data)
	fmt.Println("DTO:", dto)
	fmt.Println("ERROR:", err)
}

Essa será a saída esperada:

# go run main.go
DTO: <nil>
ERROR: 'profile.firstName' field of type 'string' is missing or empty & 'profile.email' field of type 'string' is missing or empty

Quando informado um mapa de valores corretos como esse:

...
	data := map[string]interface{}{
		"profile": map[string]interface{}{
			"firstName": "John",
			"email":     "test@email.com",
		},
	}
...

Essa será a resposta esperada:

# go run main.go
DTO: &{{John  test@email.com}}
ERROR: <nil>

Se caso você necessite usar a mesma estrutura de dados (ex.: perfil), porém dessas, ainda de forma aninhada, você precise validar somente alguns em específico, você precisa adicionar a regra nestedProps da seguinte forma:

type UpdateAccount struct {
	Profile Profile `json:"profile" validate:"nestedProps=firstName"`
}

Nesse caso temos a estrutura UpdateAccount que, por meio da regra nestedProps diz que somente o atributo firstName, dentre todos que têm validação, deve ser validado. Utilizando o seguinte conjunto de dados:

...
	data := map[string]interface{}{
		"profile": map[string]interface{}{},
	}
...

Essa é a resposta esperada:

# go run main.go
DTO: <nil>
ERROR: 'profile.firstName' field of type 'string' is missing or empty

Limitações (Problemas Conhecidos)

  1. Não é possível validar devidamente listas de objetos aninhados. Exemplo:
    type Profile struct {
    	FirstName string `json:"firstName" validate:"required"`
    	LastName  string `json:"lastName"`
    	Email     string `json:"email" validate:"required,email"`
    }
    
    type UpdateAccount struct {
    	Profiles []Profile `json:"profiles" validate:"required"`
    }
    
  2. Não é possível identificar o índice de um atributo com erro quando há uma lista de estruturas;

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL