std.json.Stringify.valueAlloc em Zig 0.16: Gerar JSON

Introdução

Gerar JSON é tão importante quanto parseá-lo. Seja para responder a uma API, salvar configurações ou exportar dados, a serialização JSON é uma operação fundamental.

Resposta rápida (Zig 0.16.0): use std.json.Stringify.valueAlloc(allocator, valor, .{}) para converter uma struct (ou std.json.Value) em []u8. O caller é dono da memória e deve chamar allocator.free. Para escrever direto em um writer, use std.json.Stringify.value. Essa API substitui o antigo std.json.stringify / stringifyAlloc.

const json = try std.json.Stringify.valueAlloc(allocator, usuario, .{});
defer allocator.free(json);

Nesta receita você serializa structs, arrays, campos opcionais e JSON dinâmico, e vê quando cair para Writer.Allocating / ArrayList managed.

Pré-requisitos

  • Zig 0.16.0, versão usada para compilar e executar os exemplos completos desta receita. Confira com zig version; veja o guia de instalação.
  • Conhecimento de parsing JSON em Zig.
  • Noção básica de allocators. Aqui usamos std.heap.DebugAllocator: GeneralPurposeAllocator não existe no Zig 0.16.0.

API moderna: Stringify.value e valueAlloc

FunçãoQuando usarRetorno
std.json.Stringify.valueAlloc(gpa, v, opts)Respostas curtas, testes, buffers owned![]u8 (caller frees)
std.json.Stringify.value(v, opts, writer)Streaming para arquivo, socket ou Writer.Allocating!void
std.json.fmt(v, opts)Interpolar JSON em print / formattersformatter

O antigo std.json.stringifyAlloc não é o caminho documentado no 0.16 — se um snippet antigo falhar no compile, troque para Stringify.valueAlloc.

Serializar uma Struct com valueAlloc

Converta uma struct Zig diretamente para JSON:

const std = @import("std");

const Usuario = struct {
    nome: []const u8,
    email: []const u8,
    idade: u32,
    ativo: bool,
};

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

    const usuario = Usuario{
        .nome = "Maria Silva",
        .email = "[email protected]",
        .idade = 28,
        .ativo = true,
    };

    // std.json.Stringify.valueAlloc — substituto de stringifyAlloc
    const json = try std.json.Stringify.valueAlloc(allocator, usuario, .{});
    defer allocator.free(json);

    std.debug.print("{s}\n", .{json});
}

Saída esperada

{"nome":"Maria Silva","email":"[email protected]","idade":28,"ativo":true}

JSON Formatado (Pretty Print)

Para JSON legível com indentação, passe Stringify.Options:

const std = @import("std");

const Configuracao = struct {
    servidor: struct {
        host: []const u8,
        porta: u16,
    },
    banco_dados: struct {
        url: []const u8,
        pool_size: u32,
    },
    debug: bool,
};

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

    const config = Configuracao{
        .servidor = .{
            .host = "0.0.0.0",
            .porta = 8080,
        },
        .banco_dados = .{
            .url = "postgresql://localhost:5432/meubanco",
            .pool_size = 10,
        },
        .debug = false,
    };

    const json = try std.json.Stringify.valueAlloc(allocator, config, .{
        .whitespace = .indent_4,
    });
    defer allocator.free(json);

    std.debug.print("{s}\n", .{json});
}

Saída esperada

{
    "servidor": {
        "host": "0.0.0.0",
        "porta": 8080
    },
    "banco_dados": {
        "url": "postgresql://localhost:5432/meubanco",
        "pool_size": 10
    },
    "debug": false
}

value + Writer.Allocating (e ArrayList managed)

Quando você já controla o buffer — ou quer reutilizar capacidade entre serializações — use Stringify.value com std.Io.Writer.Allocating. Esse é o mesmo motor interno de valueAlloc, e é o ponto em que a documentação da std cita ArrayList / array_list.Managed:

const std = @import("std");

const Evento = struct {
    tipo: []const u8,
    timestamp: i64,
    dados: []const u8,
};

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

    const evento = Evento{
        .tipo = "login",
        .timestamp = 1740100000,
        .dados = "usuario=maria",
    };

    // Caminho 1: Writer.Allocating direto (equivalente prático a valueAlloc)
    var aw: std.Io.Writer.Allocating = .init(allocator);
    defer aw.deinit();
    try std.json.Stringify.value(evento, .{}, &aw.writer);
    const json1 = try aw.toOwnedSlice();
    defer allocator.free(json1);
    std.debug.print("Allocating: {s}\n", .{json1});

    // Caminho 2: buffer ArrayList(u8) + fromArrayList
    // Útil quando o ArrayList já existe no seu pipeline (HTTP body, fila, etc.)
    var list: std.ArrayList(u8) = .empty;
    defer list.deinit(allocator);
    var aw_list = std.Io.Writer.Allocating.fromArrayList(allocator, &list);
    defer aw_list.deinit(); // libera o buffer mesmo se a serialização falhar
    try std.json.Stringify.value(evento, .{}, &aw_list.writer);
    // devolve o ArrayList atualizado; items é o JSON
    list = aw_list.toArrayList();
    std.debug.print("ArrayList: {s}\n", .{list.items});
}

Notas rápidas:

  1. No Zig 0.16.0, std.ArrayList(T) é unmanaged: o allocator vai nos métodos. Já std.json.Array é std.array_list.Managed(std.json.Value) e guarda o allocator internamente. Um buffer de bytes e um array de valores JSON são tipos distintos.
  2. fromArrayList transfere a memória de list para o writer e deixa a lista vazia. toArrayList transfere de volta e esvazia o writer. O defer aw_list.deinit() é necessário no caminho de erro; depois da transferência de volta ele é seguro porque o writer está vazio.
  3. Prefira valueAlloc quando só precisa do []u8 final. Prefira fromArrayList quando o buffer faz parte de um fluxo maior. A serialização acrescenta bytes ao buffer existente; limpe o conteúdo antes se quiser substituir o JSON anterior.

Serializar Arrays e Slices

Arrays e slices viram arrays JSON automaticamente:

const std = @import("std");

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

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

    const produtos = [_]Produto{
        .{
            .id = 1,
            .nome = "Notebook",
            .preco = 4599.90,
            .tags = &.{ "eletrônico", "computador" },
        },
        .{
            .id = 2,
            .nome = "Teclado",
            .preco = 299.90,
            .tags = &.{ "periférico", "acessório" },
        },
        .{
            .id = 3,
            .nome = "Monitor",
            .preco = 1899.90,
            .tags = &.{ "eletrônico", "display" },
        },
    };

    const json = try std.json.Stringify.valueAlloc(allocator, &produtos, .{
        .whitespace = .indent_2,
    });
    defer allocator.free(json);

    std.debug.print("{s}\n", .{json});
}

Campos Opcionais (null em JSON)

Campos ?T saem como null quando vazios (comportamento padrão de emit_null_optional_fields):

const std = @import("std");

const Perfil = struct {
    nome: []const u8,
    bio: ?[]const u8,
    site: ?[]const u8,
    seguidores: ?u32,
};

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

    const perfil_completo = Perfil{
        .nome = "João Dev",
        .bio = "Desenvolvedor Zig apaixonado",
        .site = "https://joaodev.com",
        .seguidores = 1500,
    };

    const json1 = try std.json.Stringify.valueAlloc(allocator, perfil_completo, .{
        .whitespace = .indent_2,
    });
    defer allocator.free(json1);
    std.debug.print("Completo:\n{s}\n\n", .{json1});

    const perfil_minimo = Perfil{
        .nome = "Ana",
        .bio = null,
        .site = null,
        .seguidores = null,
    };

    const json2 = try std.json.Stringify.valueAlloc(allocator, perfil_minimo, .{
        .whitespace = .indent_2,
    });
    defer allocator.free(json2);
    std.debug.print("Mínimo:\n{s}\n", .{json2});
}

Saída esperada

Completo:
{
  "nome": "João Dev",
  "bio": "Desenvolvedor Zig apaixonado",
  "site": "https://joaodev.com",
  "seguidores": 1500
}

Mínimo:
{
  "nome": "Ana",
  "bio": null,
  "site": null,
  "seguidores": null
}

Para omitir chaves nulas em vez de emitir null, passe .{ .emit_null_optional_fields = false }.

Construir JSON Dinamicamente (std.json.Value)

Para estrutura dinâmica, monte um std.json.Value e serialize com valueAlloc. Por baixo, std.json.Array usa array_list.Managed(Value):

const std = @import("std");

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

    var root = std.json.Value{ .object = .{} };
    defer root.object.deinit(allocator);

    try root.object.put(allocator, "versao", .{ .string = "1.0.0" });
    try root.object.put(allocator, "nome", .{ .string = "meu-app" });
    try root.object.put(allocator, "porta", .{ .integer = 3000 });

    var deps: std.json.ObjectMap = .{};
    defer deps.deinit(allocator);
    try deps.put(allocator, "std", .{ .string = "0.16.0" });
    try root.object.put(allocator, "dependencias", .{ .object = deps });

    // std.json.Array é uma lista managed de Value, não um buffer de bytes.
    var linguagens = std.json.Array.init(allocator);
    defer linguagens.deinit();
    try linguagens.append(.{ .string = "Zig" });
    try linguagens.append(.{ .string = "C" });
    try root.object.put(allocator, "linguagens", .{ .array = linguagens });

    const json = try std.json.Stringify.valueAlloc(allocator, root, .{
        .whitespace = .indent_2,
    });
    defer allocator.free(json);

    std.debug.print("{s}\n", .{json});
}

O objeto gerado inclui "linguagens": ["Zig", "C"]. No Zig 0.16.0, ObjectMap recebe o allocator em put e deinit, enquanto std.json.Array.init(allocator) o guarda internamente. root.object.deinit(allocator) libera apenas o armazenamento do mapa, não percorre recursivamente os objetos e arrays filhos. Por isso deps e linguagens têm seus próprios defer. As cópias inseridas em root são usadas apenas enquanto esses containers estão vivos; não libere a cópia e o original duas vezes. Aqui as strings são literais; strings alocadas separadamente exigem sua própria liberação. O []u8 retornado por valueAlloc é outra alocação independente.

Como verificar os exemplos e corrigir erros comuns

Salve qualquer exemplo completo acima como gerar-json.zig e execute:

zig version
zig run gerar-json.zig

A versão usada nesta receita é 0.16.0. std.debug.print escreve em stderr: para capturar o JSON dos exemplos que imprimem apenas JSON, use zig run gerar-json.zig 2> saida.json.

SintomaCorreção no Zig 0.16.0
std.heap não tem GeneralPurposeAllocatorUse std.heap.DebugAllocator(.{}){} e seu método allocator()
std.json não tem stringifyAllocUse std.json.Stringify.valueAlloc(allocator, valor, .{})
Stringify.value rejeita o writerPasse &aw.writer de um std.Io.Writer.Allocating, não o writer antigo
Leak ao falhar em fromArrayListAdicione defer aw_list.deinit() antes de serializar
Array JSON tratado como textoUse std.json.Array para valores; []const u8 serializa como string JSON

Migração rápida: stringifyAlloc → valueAlloc

AntesAgora (Zig 0.16)
std.json.stringifyAlloc(a, v, opts)std.json.Stringify.valueAlloc(a, v, opts)
std.json.stringify(v, opts, writer)std.json.Stringify.value(v, opts, writer)
writer “anytype” antigo (std.io.getStdOut().writer())*std.Io.Writer (API de I/O nova)

Se você só atualizou o nome da função e o compile ainda reclama do writer, migre o I/O junto — value espera o writer moderno, não o anytype antigo.

Dicas e Boas Práticas

  1. Structs tipadas primeiro: quando o schema é conhecido, serialize a struct; Value é para JSON aberto.
  2. valueAlloc para o happy path curto; value + writer para streaming e buffers reutilizados.
  3. Pretty print só em config/debug: rede e APIs ficam melhor com o default minificado.
  4. Campos opcionais: ?T + emit_null_optional_fields controlam null vs omissão.
  5. Combine com HTTP: gere o body e envie via HTTP POST.
  6. Parse do outro lado: veja Parsing JSON em Zig (parseFromSlice + deinit).

Perguntas Frequentes (FAQ)

Como gerar JSON em Zig 0.16?

Use std.json.Stringify.valueAlloc(allocator, valor, options) para obter uma []u8 JSON. O caller libera com allocator.free. Para streaming, use std.json.Stringify.value com um *std.Io.Writer.

O que é std.json.Stringify.valueAlloc?

É a API alocada de serialização JSON do Zig atual. Substitui o antigo std.json.stringifyAlloc: recebe o allocator, o valor e Stringify.Options, e devolve a string JSON em memória dinâmica.

stringifyAlloc ainda existe no Zig 0.16?

Não no caminho moderno. O módulo passou a expor std.json.Stringify com value (writer) e valueAlloc (allocator). Código antigo com stringify/stringifyAlloc precisa migrar para essas funções.

Como valueAlloc se relaciona com ArrayList managed?

Por baixo, valueAlloc usa std.Io.Writer.Allocating. Se você já tem um ArrayList(u8), pode ligar o writer com Writer.Allocating.fromArrayList e chamar Stringify.value — o padrão array_list managed aparece nos exemplos da std quando o buffer precisa crescer.

Qual a diferença entre Stringify.value e valueAlloc?

value escreve em um *std.Io.Writer (arquivo, socket, buffer). valueAlloc cria um Writer.Allocating interno, serializa e devolve []u8 owned. Prefira value em hot paths; valueAlloc em respostas curtas e testes.

Receitas Relacionadas

Referências oficiais (Zig 0.16.0)

Essas referências estão fixadas na versão dos exemplos, em vez de acompanhar mudanças da branch de desenvolvimento.

Tutoriais Relacionados

Curso planejado · Zig na prática

Já programa? Construa a sua primeira CLI em Zig.

Conheça o curso planejado: fundamentos, erros, memória, testes e build no mesmo projeto. R$ 299 na pré-venda, pagamento único.

R$ 299Pagamento único na pré-venda Conhecer o curso

Continue aprendendo Zig

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