Bibliotecas JSON em Zig — Parsing, Serialização e Streaming

Bibliotecas JSON em Zig — Parsing, Serialização e Streaming

JSON é o formato de dados mais utilizado em APIs web e configurações modernas. O Zig oferece suporte robusto a JSON através da biblioteca padrão std.json, com capacidades de parsing tipado, parsing dinâmico e serialização. O ecossistema complementa com bibliotecas especializadas para streaming de grandes volumes e integrações com outros formatos.

std.json — A Biblioteca Padrão

Parsing Tipado (Recomendado)

A forma mais segura e performática de trabalhar com JSON em Zig é o parsing tipado, onde você define a estrutura esperada:

const std = @import("std");

const Usuario = struct {
    id: u64,
    nome: []const u8,
    email: []const u8,
    idade: ?u32 = null,
    ativo: bool = true,
    tags: []const []const u8 = &.{},
};

pub fn main() !void {
    const allocator = std.heap.page_allocator;

    const json_str =
        \\{
        \\  "id": 42,
        \\  "nome": "Maria Silva",
        \\  "email": "maria@exemplo.com",
        \\  "idade": 28,
        \\  "ativo": true,
        \\  "tags": ["admin", "dev"]
        \\}
    ;

    const parsed = try std.json.parseFromSlice(
        Usuario,
        allocator,
        json_str,
        .{},
    );
    defer parsed.deinit();

    const usuario = parsed.value;
    std.debug.print("Nome: {s}\n", .{usuario.nome});
    std.debug.print("Email: {s}\n", .{usuario.email});
    std.debug.print("Idade: {?}\n", .{usuario.idade});
}

Parsing Dinâmico

Quando a estrutura do JSON não é conhecida em tempo de compilação:

pub fn main() !void {
    const allocator = std.heap.page_allocator;

    const json_str =
        \\{"chave": "valor", "numero": 42, "lista": [1, 2, 3]}
    ;

    const parsed = try std.json.parseFromSlice(
        std.json.Value,
        allocator,
        json_str,
        .{},
    );
    defer parsed.deinit();

    const root = parsed.value;

    // Acessar campos dinamicamente
    if (root.object.get("chave")) |valor| {
        switch (valor) {
            .string => |s| std.debug.print("chave = {s}\n", .{s}),
            else => {},
        }
    }

    if (root.object.get("numero")) |valor| {
        switch (valor) {
            .integer => |n| std.debug.print("numero = {}\n", .{n}),
            else => {},
        }
    }

    if (root.object.get("lista")) |valor| {
        switch (valor) {
            .array => |arr| {
                for (arr.items) |item| {
                    switch (item) {
                        .integer => |n| std.debug.print("item = {}\n", .{n}),
                        else => {},
                    }
                }
            },
            else => {},
        }
    }
}

Serialização (Zig para JSON)

const std = @import("std");

const Produto = struct {
    id: u64,
    nome: []const u8,
    preco: f64,
    disponivel: bool,
    categorias: []const []const u8,
};

pub fn main() !void {
    const produto = Produto{
        .id = 1,
        .nome = "Teclado Mecânico",
        .preco = 349.90,
        .disponivel = true,
        .categorias = &.{ "periféricos", "teclados" },
    };

    // Serializar para string
    var buf: [1024]u8 = undefined;
    var stream = std.io.fixedBufferStream(&buf);
    try std.json.stringify(produto, .{}, stream.writer());

    const json_str = stream.getWritten();
    std.debug.print("{s}\n", .{json_str});

    // Serializar com formatação (pretty print)
    var buf2: [2048]u8 = undefined;
    var stream2 = std.io.fixedBufferStream(&buf2);
    try std.json.stringify(produto, .{ .whitespace = .indent_2 }, stream2.writer());
    std.debug.print("{s}\n", .{stream2.getWritten()});
}

Opções de Parsing

const parsed = try std.json.parseFromSlice(MeuTipo, allocator, json_str, .{
    .ignore_unknown_fields = true,     // Ignorar campos desconhecidos
    .allocate = .alloc_always,         // Controle de alocação
    .max_nest_depth = 64,              // Profundidade máxima de aninhamento
});

Streaming de JSON Grande

Para arquivos JSON muito grandes que não cabem na memória:

const std = @import("std");

pub fn processarJsonGrande(reader: anytype) !void {
    var scanner = std.json.Scanner(@TypeOf(reader)).init(reader);

    while (true) {
        const token = try scanner.next();
        switch (token) {
            .object_begin => std.debug.print("Início de objeto\n", .{}),
            .object_end => std.debug.print("Fim de objeto\n", .{}),
            .array_begin => std.debug.print("Início de array\n", .{}),
            .array_end => std.debug.print("Fim de array\n", .{}),
            .string => |s| std.debug.print("String: {s}\n", .{s}),
            .number => |n| std.debug.print("Número: {s}\n", .{n}),
            .true => std.debug.print("true\n", .{}),
            .false => std.debug.print("false\n", .{}),
            .null => std.debug.print("null\n", .{}),
            .end_of_document => break,
            else => {},
        }
    }
}

Padrões Comuns

API REST com JSON

const std = @import("std");

const ApiResponse = struct {
    success: bool,
    data: ?std.json.Value = null,
    erro: ?[]const u8 = null,
    timestamp: i64,

    pub fn ok(data: anytype, allocator: std.mem.Allocator) ![]const u8 {
        const response = ApiResponse{
            .success = true,
            .timestamp = std.time.timestamp(),
        };
        _ = data;
        var buf = std.ArrayList(u8).init(allocator);
        try std.json.stringify(response, .{}, buf.writer());
        return buf.toOwnedSlice();
    }

    pub fn erro_resp(msg: []const u8, allocator: std.mem.Allocator) ![]const u8 {
        const response = ApiResponse{
            .success = false,
            .erro = msg,
            .timestamp = std.time.timestamp(),
        };
        var buf = std.ArrayList(u8).init(allocator);
        try std.json.stringify(response, .{}, buf.writer());
        return buf.toOwnedSlice();
    }
};

Configuração de Aplicação

const Config = struct {
    servidor: struct {
        host: []const u8 = "0.0.0.0",
        porta: u16 = 8080,
        workers: u32 = 4,
    } = .{},
    banco: struct {
        url: []const u8 = "postgresql://localhost/app",
        pool_size: u32 = 10,
    } = .{},
    log: struct {
        nivel: []const u8 = "info",
        formato: []const u8 = "json",
    } = .{},
};

pub fn carregarConfig(allocator: std.mem.Allocator) !Config {
    const arquivo = try std.fs.cwd().openFile("config.json", .{});
    defer arquivo.close();

    const conteudo = try arquivo.readToEndAlloc(allocator, 1024 * 1024);
    defer allocator.free(conteudo);

    const parsed = try std.json.parseFromSlice(Config, allocator, conteudo, .{
        .ignore_unknown_fields = true,
    });
    return parsed.value;
}

Performance

A implementação JSON do Zig é altamente performática:

OperaçãoZig std.jsonGo encoding/jsonPython jsonNode.js JSON
Parse 1MB~3ms~8ms~25ms~5ms
Serialize 1MB~2ms~6ms~20ms~4ms
MemóriaMínimaModeradaAltaModerada

Boas Práticas

  1. Prefira parsing tipado: Mais seguro e performático que parsing dinâmico
  2. Use ignore_unknown_fields: Permite evolução de APIs sem quebrar parsers
  3. Trate erros de parsing: JSON inválido retorna erros descritivos
  4. Streaming para arquivos grandes: Evite carregar GBs de JSON na memória
  5. Valide após parsing: O parsing verifica formato, mas a lógica de negócio precisa validação adicional

Próximos Passos

Explore outras bibliotecas de serialização como MessagePack e Protocol Buffers, os frameworks web para APIs JSON, e os clientes HTTP para consumir APIs externas. Consulte nossos tutoriais para exemplos completos.

Continue aprendendo Zig

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