Como Ler e Escrever JSON em Arquivos com Zig

Introdução

Ler e salvar JSON em arquivos é essencial para persistência de dados, arquivos de configuração, cache local e exportação de dados. Em Zig, combinamos std.fs para I/O de arquivos com std.json para serialização e deserialização.

Nesta receita, você aprenderá a ler JSON de arquivos, salvar dados como JSON e manipular arquivos de configuração.

Pré-requisitos

Ler JSON de um Arquivo

Leia e parseie um arquivo JSON completo:

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

const Config = struct {
    host: []const u8,
    porta: u16,
    debug: bool,
    max_conexoes: u32,
};

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

    // Ler o arquivo inteiro
    const data = fs.cwd().readFileAlloc(
        allocator,
        "config.json",
        1024 * 1024, // máximo 1 MB
    ) catch |err| {
        std.debug.print("Erro ao ler arquivo: {}\n", .{err});
        return;
    };
    defer allocator.free(data);

    // Parsear o JSON
    const parsed = try std.json.parseFromSlice(Config, allocator, data, .{
        .ignore_unknown_fields = true,
    });
    defer parsed.deinit();

    const config = parsed.value;
    std.debug.print("Configuração carregada:\n", .{});
    std.debug.print("  Host: {s}\n", .{config.host});
    std.debug.print("  Porta: {d}\n", .{config.porta});
    std.debug.print("  Debug: {}\n", .{config.debug});
    std.debug.print("  Max conexões: {d}\n", .{config.max_conexoes});
}

Salvar JSON em um Arquivo

Serialize uma struct e salve em disco:

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

const AppState = struct {
    versao: []const u8,
    ultimo_acesso: i64,
    usuarios_ativos: u32,
    configuracoes: struct {
        tema: []const u8,
        idioma: []const u8,
        notificacoes: bool,
    },
};

fn saveJson(comptime T: type, data: T, path: []const u8) !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    // Serializar para JSON com formatação
    const json = try std.json.stringifyAlloc(allocator, data, .{
        .whitespace = .indent_2,
    });
    defer allocator.free(json);

    // Escrever no arquivo
    const file = try fs.cwd().createFile(path, .{});
    defer file.close();

    try file.writeAll(json);
    std.debug.print("Salvo em '{s}' ({d} bytes)\n", .{ path, json.len });
}

pub fn main() !void {
    const state = AppState{
        .versao = "2.1.0",
        .ultimo_acesso = 1740100000,
        .usuarios_ativos = 42,
        .configuracoes = .{
            .tema = "escuro",
            .idioma = "pt-BR",
            .notificacoes = true,
        },
    };

    try saveJson(AppState, state, "estado.json");
}

Arquivo gerado (estado.json)

{
  "versao": "2.1.0",
  "ultimo_acesso": 1740100000,
  "usuarios_ativos": 42,
  "configuracoes": {
    "tema": "escuro",
    "idioma": "pt-BR",
    "notificacoes": true
  }
}

Gerenciador de Configuração Completo

Uma classe que carrega, modifica e salva configurações JSON:

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

const Config = struct {
    servidor: struct {
        host: []const u8 = "localhost",
        porta: u16 = 8080,
    } = .{},
    log: struct {
        nivel: []const u8 = "info",
        arquivo: []const u8 = "app.log",
    } = .{},
    max_threads: u32 = 4,
};

const ConfigManager = struct {
    config: Config,
    path: []const u8,
    allocator: std.mem.Allocator,
    _parsed: ?std.json.Parsed(Config),

    pub fn init(allocator: std.mem.Allocator, path: []const u8) ConfigManager {
        return .{
            .config = .{},
            .path = path,
            .allocator = allocator,
            ._parsed = null,
        };
    }

    pub fn load(self: *ConfigManager) !void {
        const data = fs.cwd().readFileAlloc(
            self.allocator,
            self.path,
            1024 * 1024,
        ) catch |err| {
            if (err == error.FileNotFound) {
                std.debug.print("Arquivo não encontrado. Usando padrões.\n", .{});
                self.config = .{};
                return;
            }
            return err;
        };
        defer self.allocator.free(data);

        if (self._parsed) |*p| p.deinit();

        self._parsed = try std.json.parseFromSlice(Config, self.allocator, data, .{
            .ignore_unknown_fields = true,
        });
        self.config = self._parsed.?.value;
    }

    pub fn save(self: *ConfigManager) !void {
        const json = try std.json.stringifyAlloc(self.allocator, self.config, .{
            .whitespace = .indent_2,
        });
        defer self.allocator.free(json);

        const file = try fs.cwd().createFile(self.path, .{});
        defer file.close();

        try file.writeAll(json);
    }

    pub fn deinit(self: *ConfigManager) void {
        if (self._parsed) |*p| p.deinit();
    }
};

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

    var mgr = ConfigManager.init(allocator, "app_config.json");
    defer mgr.deinit();

    // Tentar carregar configuração existente
    try mgr.load();

    std.debug.print("Configuração atual:\n", .{});
    std.debug.print("  Host: {s}\n", .{mgr.config.servidor.host});
    std.debug.print("  Porta: {d}\n", .{mgr.config.servidor.porta});
    std.debug.print("  Log: {s}\n", .{mgr.config.log.nivel});
    std.debug.print("  Threads: {d}\n", .{mgr.config.max_threads});

    // Salvar a configuração
    try mgr.save();
    std.debug.print("\nConfiguração salva com sucesso!\n", .{});
}

Atualizar JSON Existente

Leia, modifique e salve novamente:

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

fn updateJsonField(
    allocator: std.mem.Allocator,
    path: []const u8,
    field: []const u8,
    new_value: std.json.Value,
) !void {
    // Ler arquivo existente
    const data = try fs.cwd().readFileAlloc(allocator, path, 1024 * 1024);
    defer allocator.free(data);

    // Parsear como valor dinâmico
    var parsed = try std.json.parseFromSlice(std.json.Value, allocator, data, .{});
    defer parsed.deinit();

    // Modificar o campo
    if (parsed.value == .object) {
        try parsed.value.object.put(field, new_value);
    }

    // Serializar de volta
    const json = try std.json.stringifyAlloc(allocator, parsed.value, .{
        .whitespace = .indent_2,
    });
    defer allocator.free(json);

    // Salvar
    const file = try fs.cwd().createFile(path, .{});
    defer file.close();
    try file.writeAll(json);

    std.debug.print("Campo '{s}' atualizado com sucesso.\n", .{field});
}

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

    // Primeiro, criar o arquivo
    const initial = "{\"versao\": \"1.0.0\", \"porta\": 3000}";
    {
        const file = try fs.cwd().createFile("dados.json", .{});
        defer file.close();
        try file.writeAll(initial);
    }

    // Atualizar um campo
    try updateJsonField(
        allocator,
        "dados.json",
        "porta",
        .{ .integer = 8080 },
    );
}

Dicas e Boas Práticas

  1. Limite o tamanho: Sempre defina um limite máximo ao ler arquivos para evitar uso excessivo de memória.

  2. Trate FileNotFound: Para configurações, use valores padrão quando o arquivo não existir.

  3. Use formatação para configs: Para arquivos editados manualmente, use indentação (.whitespace = .indent_2).

  4. Atomicidade: Para dados críticos, escreva em um arquivo temporário e renomeie para evitar corrupção.

  5. Gerencie memória: Use o gerenciamento de memória do Zig cuidadosamente com parsed values.

Receitas Relacionadas

Tutoriais Relacionados

Continue aprendendo Zig

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