Como Fazer Streaming de JSON em Zig

Introdução

Quando você precisa processar arquivos JSON muito grandes (centenas de megabytes ou gigabytes), carregar tudo na memória não é viável. O streaming JSON permite processar os dados token por token, consumindo memória constante independente do tamanho do arquivo.

Em Zig, std.json.Scanner oferece um parser de baixo nível que emite tokens conforme lê o JSON, permitindo processamento eficiente de dados massivos.

Pré-requisitos

Scanner JSON Básico

Use o scanner para processar JSON token por token:

const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    const json_str =
        \\{"nome": "Maria", "idade": 28, "ativo": true}
    ;

    // Criar scanner
    var scanner = std.json.Scanner.initCompleteInput(allocator, json_str);
    defer scanner.deinit();

    // Processar tokens
    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("Boolean: true\n", .{}),
            .false => std.debug.print("Boolean: false\n", .{}),
            .null => std.debug.print("Null\n", .{}),
            .end_of_document => {
                std.debug.print("Fim do documento\n", .{});
                break;
            },
            else => {},
        }
    }
}

Saída esperada

Início de objeto
String: "nome"
String: "Maria"
String: "idade"
Número: 28
String: "ativo"
Boolean: true
Fim de objeto
Fim do documento

Streaming de Arquivo Grande

Processe um arquivo JSON grande sem carregar tudo na memória:

const std = @import("std");
const fs = std.fs;

const Estatisticas = struct {
    total_objetos: u64 = 0,
    total_strings: u64 = 0,
    total_numeros: u64 = 0,
    total_booleans: u64 = 0,
    total_nulls: u64 = 0,
    profundidade_max: u32 = 0,
    profundidade_atual: u32 = 0,
};

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    // Simular dados grandes criando o arquivo
    const file = try fs.cwd().createFile("dados_grandes.json", .{});
    {
        defer file.close();
        const writer = file.writer();
        try writer.writeAll("[");
        for (0..1000) |i| {
            if (i > 0) try writer.writeAll(",");
            try writer.print(
                \\{{"id":{d},"nome":"Usuario {d}","ativo":true,"pontos":{d}}}
            , .{ i, i, i * 10 });
        }
        try writer.writeAll("]");
    }

    // Agora ler em streaming
    const data = try fs.cwd().readFileAlloc(allocator, "dados_grandes.json", 100 * 1024 * 1024);
    defer allocator.free(data);

    var scanner = std.json.Scanner.initCompleteInput(allocator, data);
    defer scanner.deinit();

    var stats = Estatisticas{};

    while (true) {
        const token = try scanner.next();
        switch (token) {
            .object_begin => {
                stats.total_objetos += 1;
                stats.profundidade_atual += 1;
                if (stats.profundidade_atual > stats.profundidade_max) {
                    stats.profundidade_max = stats.profundidade_atual;
                }
            },
            .object_end => stats.profundidade_atual -= 1,
            .array_begin => {
                stats.profundidade_atual += 1;
                if (stats.profundidade_atual > stats.profundidade_max) {
                    stats.profundidade_max = stats.profundidade_atual;
                }
            },
            .array_end => stats.profundidade_atual -= 1,
            .string => stats.total_strings += 1,
            .number => stats.total_numeros += 1,
            .true, .false => stats.total_booleans += 1,
            .null => stats.total_nulls += 1,
            .end_of_document => break,
            else => {},
        }
    }

    std.debug.print("Estatísticas do JSON:\n", .{});
    std.debug.print("  Objetos: {d}\n", .{stats.total_objetos});
    std.debug.print("  Strings: {d}\n", .{stats.total_strings});
    std.debug.print("  Números: {d}\n", .{stats.total_numeros});
    std.debug.print("  Booleans: {d}\n", .{stats.total_booleans});
    std.debug.print("  Nulls: {d}\n", .{stats.total_nulls});
    std.debug.print("  Profundidade máxima: {d}\n", .{stats.profundidade_max});
}

Extrair Campos Específicos em Streaming

Procure campos específicos sem parsear o objeto inteiro:

const std = @import("std");

fn findFieldValue(
    allocator: std.mem.Allocator,
    json_str: []const u8,
    target_field: []const u8,
) !?[]const u8 {
    var scanner = std.json.Scanner.initCompleteInput(allocator, json_str);
    defer scanner.deinit();

    var depth: u32 = 0;
    var in_target_object = false;
    var found_key = false;

    while (true) {
        const token = try scanner.next();
        switch (token) {
            .object_begin => {
                depth += 1;
                if (depth == 1) in_target_object = true;
            },
            .object_end => {
                depth -= 1;
                if (depth == 0) in_target_object = false;
            },
            .string => |s| {
                if (found_key) {
                    // Este é o valor que procuramos
                    return try allocator.dupe(u8, s);
                }
                if (in_target_object and depth == 1 and std.mem.eql(u8, s, target_field)) {
                    found_key = true;
                }
            },
            .number => |n| {
                if (found_key) {
                    return try allocator.dupe(u8, n);
                }
            },
            .end_of_document => break,
            else => {
                if (found_key) found_key = false;
            },
        }
    }

    return null;
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    const json =
        \\{
        \\  "id": "abc123",
        \\  "nome": "Produto Especial",
        \\  "preco": "299.90",
        \\  "categoria": "eletrônicos",
        \\  "descricao": "Um produto muito especial"
        \\}
    ;

    // Procurar apenas o campo "nome"
    if (try findFieldValue(allocator, json, "nome")) |valor| {
        defer allocator.free(valor);
        std.debug.print("Nome encontrado: {s}\n", .{valor});
    }

    if (try findFieldValue(allocator, json, "preco")) |valor| {
        defer allocator.free(valor);
        std.debug.print("Preço encontrado: {s}\n", .{valor});
    }

    if (try findFieldValue(allocator, json, "inexistente")) |_| {
        std.debug.print("Não deveria chegar aqui\n", .{});
    } else {
        std.debug.print("Campo 'inexistente' não encontrado (esperado)\n", .{});
    }
}

Contar Elementos em Array Sem Carregar Tudo

Conte elementos sem alocar memória para o array inteiro:

const std = @import("std");

fn countArrayElements(allocator: std.mem.Allocator, json_str: []const u8) !u64 {
    var scanner = std.json.Scanner.initCompleteInput(allocator, json_str);
    defer scanner.deinit();

    var count: u64 = 0;
    var depth: u32 = 0;
    var in_root_array = false;

    while (true) {
        const token = try scanner.next();
        switch (token) {
            .array_begin => {
                depth += 1;
                if (depth == 1) in_root_array = true;
            },
            .array_end => {
                depth -= 1;
                if (depth == 0) in_root_array = false;
            },
            .object_begin => {
                if (in_root_array and depth == 1) count += 1;
                depth += 1;
            },
            .object_end => depth -= 1,
            .string, .number, .true, .false, .null => {
                if (in_root_array and depth == 1) count += 1;
            },
            .end_of_document => break,
            else => {},
        }
    }

    return count;
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    const json = "[1, 2, 3, {\"a\": 1}, [5, 6], \"texto\", null, true]";

    const count = try countArrayElements(allocator, json);
    std.debug.print("Elementos no array: {d}\n", .{count});
}

Saída esperada

Elementos no array: 8

Dicas e Boas Práticas

  1. Use streaming para arquivos grandes: Se o JSON tem mais de alguns MB, streaming é mais eficiente.

  2. Memória constante: O scanner usa memória proporcional à profundidade de aninhamento, não ao tamanho do arquivo.

  3. Combine com parsing tipado: Use streaming para encontrar seções e parsing tipado para processar sub-objetos.

  4. Trate erros de sintaxe: O scanner retorna erros quando encontra JSON inválido.

  5. Performance: O scanner é significativamente mais rápido para arquivos grandes pois evita alocações.

Receitas Relacionadas

Tutoriais Relacionados

Continue aprendendo Zig

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