Como Validar JSON em Zig

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

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

  1. Combine parsing tipado com validação manual: O parsing tipado verifica tipos, e você complementa com regras de negócio.

  2. Retorne todos os erros: Colete todos os erros de validação em vez de parar no primeiro.

  3. Mensagens claras: Use mensagens de erro descritivas que indiquem o campo e o problema.

  4. Valide na fronteira: Valide JSON recebido de fontes externas antes de usar internamente.

  5. Trate erros adequadamente: Use error sets customizados para erros de validação.

Receitas Relacionadas

Tutoriais Relacionados

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.