Introdução
Validar JSON é crucial quando você recebe dados de fontes externas como APIs, formulários ou arquivos de configuração. Validação vai além de verificar se a sintaxe é correta: inclui checar tipos, valores obrigatórios, intervalos e regras de negócio.
Nesta receita, você aprenderá diferentes estratégias para validar JSON em Zig.
Pré-requisitos
- Zig instalado (versão 0.13+). Veja o guia de instalação
- Conhecimento de parsing JSON em Zig
Validar Sintaxe JSON
A forma mais básica de validação: verificar se o JSON é sintaticamente válido:
const std = @import("std");
fn isValidJson(allocator: std.mem.Allocator, json_str: []const u8) bool {
var scanner = std.json.Scanner.initCompleteInput(allocator, json_str);
defer scanner.deinit();
while (true) {
const token = scanner.next() catch return false;
if (token == .end_of_document) return true;
}
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const test_cases = [_]struct { json: []const u8, descricao: []const u8 }{
.{ .json = "{\"nome\": \"Maria\"}", .descricao = "Objeto simples" },
.{ .json = "[1, 2, 3]", .descricao = "Array simples" },
.{ .json = "{nome: Maria}", .descricao = "Chaves sem aspas" },
.{ .json = "{\"a\": }", .descricao = "Valor faltando" },
.{ .json = "", .descricao = "String vazia" },
.{ .json = "null", .descricao = "Valor null" },
.{ .json = "{\"a\": 1,}", .descricao = "Vírgula extra" },
};
for (&test_cases) |tc| {
const valido = isValidJson(allocator, tc.json);
std.debug.print("{s}: {s}\n", .{
tc.descricao,
if (valido) "VÁLIDO" else "INVÁLIDO",
});
}
}
Saída esperada
Objeto simples: VÁLIDO
Array simples: VÁLIDO
Chaves sem aspas: INVÁLIDO
Valor faltando: INVÁLIDO
String vazia: INVÁLIDO
Valor null: VÁLIDO
Vírgula extra: INVÁLIDO
Validar Estrutura e Tipos
Verifique se o JSON tem a estrutura esperada:
const std = @import("std");
const ValidationError = error{
CampoObrigatorio,
TipoInvalido,
ValorForaDoIntervalo,
FormatoInvalido,
JsonInvalido,
};
const ValidacaoResultado = struct {
valido: bool,
erros: []const []const u8,
};
fn validateUsuario(allocator: std.mem.Allocator, json_str: []const u8) !ValidacaoResultado {
var erros = std.ArrayList([]const u8).init(allocator);
errdefer erros.deinit();
// Tentar parsear como valor dinâmico
const parsed = std.json.parseFromSlice(
std.json.Value,
allocator,
json_str,
.{},
) catch {
try erros.append("JSON sintaticamente inválido");
return .{ .valido = false, .erros = try erros.toOwnedSlice() };
};
defer parsed.deinit();
const root = parsed.value;
// Deve ser um objeto
if (root != .object) {
try erros.append("Raiz deve ser um objeto JSON");
return .{ .valido = false, .erros = try erros.toOwnedSlice() };
}
const obj = root.object;
// Validar campo 'nome' (obrigatório, string)
if (obj.get("nome")) |nome| {
if (nome != .string) {
try erros.append("'nome' deve ser uma string");
} else if (nome.string.len < 2) {
try erros.append("'nome' deve ter pelo menos 2 caracteres");
}
} else {
try erros.append("Campo 'nome' é obrigatório");
}
// Validar campo 'email' (obrigatório, string, deve conter @)
if (obj.get("email")) |email| {
if (email != .string) {
try erros.append("'email' deve ser uma string");
} else if (std.mem.indexOf(u8, email.string, "@") == null) {
try erros.append("'email' deve conter '@'");
}
} else {
try erros.append("Campo 'email' é obrigatório");
}
// Validar campo 'idade' (obrigatório, número, 0-150)
if (obj.get("idade")) |idade| {
if (idade != .integer) {
try erros.append("'idade' deve ser um número inteiro");
} else if (idade.integer < 0 or idade.integer > 150) {
try erros.append("'idade' deve estar entre 0 e 150");
}
} else {
try erros.append("Campo 'idade' é obrigatório");
}
return .{
.valido = erros.items.len == 0,
.erros = try erros.toOwnedSlice(),
};
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const test_jsons = [_]struct { json: []const u8, descricao: []const u8 }{
.{
.json = "{\"nome\": \"Maria Silva\", \"email\": \"maria@ex.com\", \"idade\": 28}",
.descricao = "Dados válidos",
},
.{
.json = "{\"nome\": \"M\", \"email\": \"invalido\", \"idade\": 200}",
.descricao = "Dados com erros",
},
.{
.json = "{\"email\": \"maria@ex.com\"}",
.descricao = "Campos faltando",
},
};
for (&test_jsons) |tc| {
std.debug.print("--- {s} ---\n", .{tc.descricao});
const resultado = try validateUsuario(allocator, tc.json);
defer allocator.free(resultado.erros);
if (resultado.valido) {
std.debug.print(" VÁLIDO\n", .{});
} else {
std.debug.print(" INVÁLIDO ({d} erros):\n", .{resultado.erros.len});
for (resultado.erros) |erro| {
std.debug.print(" - {s}\n", .{erro});
}
}
std.debug.print("\n", .{});
}
}
Saída esperada
--- Dados válidos ---
VÁLIDO
--- Dados com erros ---
INVÁLIDO (3 erros):
- 'nome' deve ter pelo menos 2 caracteres
- 'email' deve conter '@'
- 'idade' deve estar entre 0 e 150
--- Campos faltando ---
INVÁLIDO (2 erros):
- Campo 'nome' é obrigatório
- Campo 'idade' é obrigatório
Validação via Parsing Tipado
O parsing tipado do Zig já faz validação de tipo automaticamente:
const std = @import("std");
const Pedido = struct {
id: u64,
produto: []const u8,
quantidade: u32,
preco_unitario: f64,
};
fn validarPedido(allocator: std.mem.Allocator, json_str: []const u8) !?Pedido {
const parsed = std.json.parseFromSlice(Pedido, allocator, json_str, .{
.ignore_unknown_fields = true,
}) catch |err| {
switch (err) {
error.UnexpectedCharacter => {
std.debug.print("Erro de sintaxe JSON\n", .{});
},
error.MissingField => {
std.debug.print("Campo obrigatório faltando\n", .{});
},
error.InvalidCharacter => {
std.debug.print("Tipo de dado incompatível\n", .{});
},
else => {
std.debug.print("Erro de parsing: {}\n", .{err});
},
}
return null;
};
defer parsed.deinit();
const pedido = parsed.value;
// Validações de negócio
if (pedido.quantidade == 0) {
std.debug.print("Erro: quantidade deve ser maior que zero\n", .{});
return null;
}
if (pedido.preco_unitario <= 0) {
std.debug.print("Erro: preço deve ser positivo\n", .{});
return null;
}
return Pedido{
.id = pedido.id,
.produto = try allocator.dupe(u8, pedido.produto),
.quantidade = pedido.quantidade,
.preco_unitario = pedido.preco_unitario,
};
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Pedido válido
const json_ok = "{\"id\": 1, \"produto\": \"Teclado\", \"quantidade\": 2, \"preco_unitario\": 149.90}";
if (try validarPedido(allocator, json_ok)) |pedido| {
defer allocator.free(pedido.produto);
std.debug.print("Pedido #{d}: {d}x {s} = R${d:.2}\n", .{
pedido.id,
pedido.quantidade,
pedido.produto,
@as(f64, @floatFromInt(pedido.quantidade)) * pedido.preco_unitario,
});
}
// Pedido com campo faltando
const json_missing = "{\"id\": 2, \"produto\": \"Mouse\"}";
_ = try validarPedido(allocator, json_missing);
// Pedido com quantidade zero
const json_zero = "{\"id\": 3, \"produto\": \"Monitor\", \"quantidade\": 0, \"preco_unitario\": 999.90}";
_ = try validarPedido(allocator, json_zero);
}
Dicas e Boas Práticas
Combine parsing tipado com validação manual: O parsing tipado verifica tipos, e você complementa com regras de negócio.
Retorne todos os erros: Colete todos os erros de validação em vez de parar no primeiro.
Mensagens claras: Use mensagens de erro descritivas que indiquem o campo e o problema.
Valide na fronteira: Valide JSON recebido de fontes externas antes de usar internamente.
Trate erros adequadamente: Use error sets customizados para erros de validação.
Receitas Relacionadas
- Como parsear JSON em Zig - Fundamentos do parsing
- Como gerar JSON em Zig - Serialização
- Como ler/escrever JSON em arquivos - I/O de arquivos
- Como usar error sets customizados - Erros personalizados