JSON é o formato de dados mais popular da web moderna. APIs REST, configurações, logs, e troca de dados entre sistemas — tudo usa JSON. Saber trabalhar com JSON é uma habilidade essencial para qualquer desenvolvedor Zig.
Neste tutorial completo, você vai aprender tudo sobre parsing JSON em Zig — desde o básico até casos avançados com APIs reais. Ao final, será capaz de consumir qualquer API JSON e criar seus próprios serviços.
Por que JSON em Zig?
O Zig oferece um sistema de parsing JSON poderoso e eficiente através do módulo std.json:
| Característica | std.json Zig | Outras Linguagens |
|---|---|---|
| Zero allocations | ✅ Possível | ⚠️ Raro |
| Type safety | ✅ Compile-time | ⚠️ Runtime |
| Performance | ✅ Nativa | Variável |
| Streaming | ✅ Suportado | ⚠️ Limitado |
| Sem dependências | ✅ Built-in | ❌ Precisa libs |
Abordagens do std.json
- Structs tipadas — Parse direto para structs (mais eficiente, type safe)
- Value dinâmico — Trabalhe com JSON genérico (flexível, menos type safe)
- Streaming — Parse grandes arquivos sem carregar tudo na memória
Vamos explorar cada uma.
Pré-requisitos
Antes de começar:
- Zig instalado (0.13.0+) — veja Como Instalar o Zig
- Conhecimento básico de Zig — structs, allocators, error handling
- Familiaridade com JSON — objetos, arrays, tipos de dados
Setup do Projeto
mkdir json-tutorial
cd json-tutorial
zig init
Parte 1: Parsing JSON Básico
1.1 Parse para Struct (Mais Comum)
A forma mais eficiente de trabalhar com JSON é mapear diretamente para structs:
// src/main.zig
const std = @import("std");
// Struct que representa o JSON
const User = struct {
id: u32,
name: []const u8,
email: []const u8,
active: bool,
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// JSON de exemplo
const json_str =
\\{
\\ "id": 1,
\\ "name": "João Silva",
\\ "email": "joao@exemplo.com",
\\ "active": true
\\}
;
// Parse o JSON para a struct User
const parsed = try std.json.parseFromSlice(
User,
allocator,
json_str,
.{}, // opções padrão
);
defer parsed.deinit(); // Libera memória
// Acessa os dados
const user = parsed.value;
std.debug.print("ID: {d}\n", .{user.id});
std.debug.print("Nome: {s}\n", .{user.name});
std.debug.print("Email: {s}\n", .{user.email});
std.debug.print("Ativo: {}\n", .{user.active});
}
Compile e execute:
zig build run
Saída:
ID: 1
Nome: João Silva
Email: joao@exemplo.com
Ativo: true
1.2 Parse de Arrays
JSON com arrays é igualmente simples:
const std = @import("std");
const Product = struct {
id: u32,
name: []const u8,
price: f64,
tags: []const []const u8, // Array de strings
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const json_str =
\\[
\\ {"id": 1, "name": "Notebook", "price": 4500.00, "tags": ["eletrônicos", "computadores"]},
\\ {"id": 2, "name": "Mouse", "price": 89.90, "tags": ["eletrônicos", "acessórios"]},
\\ {"id": 3, "name": "Teclado", "price": 199.00, "tags": ["eletrônicos"]}
\\]
;
const parsed = try std.json.parseFromSlice(
[]const Product,
allocator,
json_str,
.{},
);
defer parsed.deinit();
std.debug.print("Produtos encontrados: {d}\n\n", .{parsed.value.len});
for (parsed.value) |product| {
std.debug.print("📦 {s}\n", .{product.name});
std.debug.print(" Preço: R$ {d:.2}\n", .{product.price});
std.debug.print(" Tags: ", .{});
for (product.tags) |tag| {
std.debug.print("{s} ", .{tag});
}
std.debug.print("\n\n", .{});
}
}
1.3 Campos Opcionais
Muitas APIs retornam campos que podem estar ausentes. Use ?T para opcional:
const std = @import("std");
const Profile = struct {
username: []const u8,
bio: ?[]const u8, // Pode ser null
website: ?[]const u8, // Pode ser null
followers: u32,
following: u32,
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// JSON com campos ausentes
const json_str =
\\{
\\ "username": "maria_dev",
\\ "bio": "Desenvolvedora Zig entusiasta",
\\ "followers": 1500,
\\ "following": 230
\\}
;
const parsed = try std.json.parseFromSlice(
Profile,
allocator,
json_str,
.{},
);
defer parsed.deinit();
const profile = parsed.value;
std.debug.print("👤 @{s}\n", .{profile.username});
if (profile.bio) |bio| {
std.debug.print(" Bio: {s}\n", .{bio});
} else {
std.debug.print(" Bio: (não informada)\n", .{});
}
if (profile.website) |site| {
std.debug.print(" Site: {s}\n", .{site});
} else {
std.debug.print(" Site: (não informado)\n", .{});
}
std.debug.print(" Seguidores: {d} | Seguindo: {d}\n", .{
profile.followers,
profile.following,
});
}
1.4 Mapeamento de Nomes
Quando o JSON usa convenção diferente (snake_case vs camelCase):
const std = @import("std");
const ApiResponse = struct {
// Mapeia "user_id" no JSON para "user_id" na struct
user_id: u32,
// Mapeia "created_at" no JSON para "created_at" na struct
created_at: []const u8,
// Mapeia "isActive" no JSON para "is_active" na struct
is_active: bool,
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// JSON com snake_case
const json_str =
\\{
\\ "user_id": 42,
\\ "created_at": "2026-02-10T14:30:00Z",
\\ "is_active": true
\\}
;
// Use .allocate para mapear campos com nomes diferentes
const parsed = try std.json.parseFromSlice(
ApiResponse,
allocator,
json_str,
.{
.allocate = .alloc_always, // Necessário para strings
},
);
defer parsed.deinit();
const data = parsed.value;
std.debug.print("User ID: {d}\n", .{data.user_id});
std.debug.print("Created: {s}\n", .{data.created_at});
std.debug.print("Active: {}\n", .{data.is_active});
}
Parte 2: Serialização (Struct → JSON)
2.1 Serialize Struct para JSON
Converter dados para JSON é igualmente simples:
const std = @import("std");
const Task = struct {
id: u32,
title: []const u8,
completed: bool,
priority: []const u8,
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const task = Task{
.id = 1,
.title = "Implementar parsing JSON",
.completed = false,
.priority = "high",
};
// Serializa para string JSON
const json_str = try std.json.stringifyAlloc(allocator, task, .{});
defer allocator.free(json_str);
std.debug.print("JSON gerado:\n{s}\n", .{json_str});
}
Saída:
{"id":1,"title":"Implementar parsing JSON","completed":false,"priority":"high"}
2.2 Pretty Print (JSON Formatado)
Para output legível, use .whitespace:
const std = @import("std");
const Project = struct {
name: []const u8,
version: []const u8,
dependencies: []const []const u8,
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const project = Project{
.name = "meu-app-zig",
.version = "1.0.0",
.dependencies = &.{ "zig-clap", "zig-json" },
};
// Pretty print com indentação
const json_str = try std.json.stringifyAlloc(
allocator,
project,
.{ .whitespace = .indent_2 },
);
defer allocator.free(json_str);
std.debug.print("{s}\n", .{json_str});
}
Saída:
{
"name": "meu-app-zig",
"version": "1.0.0",
"dependencies": [
"zig-clap",
"zig-json"
]
}
2.3 Escrever JSON em Arquivo
const std = @import("std");
const Config = struct {
server_host: []const u8,
server_port: u16,
debug_mode: bool,
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const config = Config{
.server_host = "localhost",
.server_port = 8080,
.debug_mode = true,
};
// Cria arquivo
const file = try std.fs.cwd().createFile("config.json", .{});
defer file.close();
// Escreve JSON formatado no arquivo
try std.json.stringify(config, .{ .whitespace = .indent_2 }, file.writer());
std.debug.print("✅ Configuração salva em config.json\n", .{});
}
Parte 3: JSON Dinâmico (std.json.Value)
Quando você não conhece a estrutura do JSON antecipadamente, use std.json.Value:
3.1 Trabalhando com JSON Genérico
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const json_str =
\\{
\\ "status": "success",
\\ "data": {
\\ "users": [
\\ {"id": 1, "name": "Alice"},
\\ {"id": 2, "name": "Bob"}
\\ ],
\\ "total": 2
\\ },
\\ "meta": {
\\ "page": 1,
\\ "per_page": 10
\\ }
\\}
;
// Parse para Value (JSON dinâmico)
const parsed = try std.json.parseFromSlice(
std.json.Value,
allocator,
json_str,
.{},
);
defer parsed.deinit();
const root = parsed.value;
// Acessa campos dinamicamente
const status = root.object.get("status").?.string;
std.debug.print("Status: {s}\n", .{status});
const data = root.object.get("data").?;
const total = data.object.get("total").?.integer;
std.debug.print("Total de usuários: {d}\n", .{total});
const users = data.object.get("users").?.array;
std.debug.print("Usuários:\n", .{});
for (users.items) |user| {
const id = user.object.get("id").?.integer;
const name = user.object.get("name").?.string;
std.debug.print(" - {d}: {s}\n", .{ id, name });
}
}
3.2 Modificar JSON Dinâmico
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Cria um objeto JSON programaticamente
var root = std.json.ObjectMap.init(allocator);
defer root.deinit();
// Adiciona campos
try root.put("name", std.json.Value{ .string = "Meu App" });
try root.put("version", std.json.Value{ .string = "1.0.0" });
try root.put("count", std.json.Value{ .integer = 42 });
// Cria array
var tags = std.json.Array.init(allocator);
try tags.append(std.json.Value{ .string = "zig" });
try tags.append(std.json.Value{ .string = "tutorial" });
try root.put("tags", std.json.Value{ .array = tags });
// Cria objeto aninhado
var config = std.json.ObjectMap.init(allocator);
try config.put("debug", std.json.Value{ .bool = true });
try config.put("port", std.json.Value{ .integer = 8080 });
try root.put("config", std.json.Value{ .object = config });
// Serializa
const value = std.json.Value{ .object = root };
const json_str = try std.json.stringifyAlloc(allocator, value, .{ .whitespace = .indent_2 });
defer allocator.free(json_str);
std.debug.print("{s}\n", .{json_str});
}
Parte 4: Exemplo Real — Cliente de API
Vamos criar um cliente completo para uma API REST:
// src/api_client.zig
const std = @import("std");
const http = std.http;
// Tipos da API
pub const Post = struct {
userId: u32,
id: u32,
title: []const u8,
body: []const u8,
};
pub const CreatePostRequest = struct {
userId: u32,
title: []const u8,
body: []const u8,
};
pub const ApiClient = struct {
allocator: std.mem.Allocator,
base_url: []const u8,
client: http.Client,
pub fn init(allocator: std.mem.Allocator, base_url: []const u8) ApiClient {
return .{
.allocator = allocator,
.base_url = base_url,
.client = http.Client{ .allocator = allocator },
};
}
pub fn deinit(self: *ApiClient) void {
self.client.deinit();
}
/// GET /posts/{id}
pub fn getPost(self: *ApiClient, id: u32) !Post {
const url = try std.fmt.allocPrint(
self.allocator,
"{s}/posts/{d}",
.{ self.base_url, id },
);
defer self.allocator.free(url);
const response = try self.makeRequest(.GET, url, null);
defer self.allocator.free(response);
const parsed = try std.json.parseFromSlice(
Post,
self.allocator,
response,
.{},
);
defer parsed.deinit();
// Copia os dados para retornar (strings são alocadas)
return Post{
.userId = parsed.value.userId,
.id = parsed.value.id,
.title = try self.allocator.dupe(u8, parsed.value.title),
.body = try self.allocator.dupe(u8, parsed.value.body),
};
}
/// POST /posts
pub fn createPost(self: *ApiClient, request: CreatePostRequest) !Post {
const url = try std.fmt.allocPrint(
self.allocator,
"{s}/posts",
.{self.base_url},
);
defer self.allocator.free(url);
// Serializa request
const body = try std.json.stringifyAlloc(self.allocator, request, .{});
defer self.allocator.free(body);
const response = try self.makeRequest(.POST, url, body);
defer self.allocator.free(response);
const parsed = try std.json.parseFromSlice(
Post,
self.allocator,
response,
.{},
);
defer parsed.deinit();
return Post{
.userId = parsed.value.userId,
.id = parsed.value.id,
.title = try self.allocator.dupe(u8, parsed.value.title),
.body = try self.allocator.dupe(u8, parsed.value.body),
};
}
fn makeRequest(
self: *ApiClient,
method: http.Method,
url: []const u8,
body: ?[]const u8,
) ![]const u8 {
const uri = try std.Uri.parse(url);
var server_header_buffer: [4096]u8 = undefined;
var request = try self.client.open(
method,
uri,
.{
.server_header_buffer = &server_header_buffer,
.headers = .{
.content_type = if (body != null) .{ .override = "application/json" } else null,
},
},
);
defer request.deinit();
// Escreve body se presente
if (body) |b| {
request.transfer_encoding = .{ .content_length = b.len };
try request.send();
try request.writeAll(b);
} else {
request.transfer_encoding = .chunked;
try request.send();
}
try request.finish();
try request.wait();
// Lê resposta
const response_body = try request.reader().readAllAlloc(
self.allocator,
1024 * 1024,
);
return response_body;
}
};
Usando o Cliente
// src/main.zig
const std = @import("std");
const ApiClient = @import("api_client.zig").ApiClient;
const CreatePostRequest = @import("api_client.zig").CreatePostRequest;
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Inicializa cliente (usando JSONPlaceholder para testes)
var client = ApiClient.init(allocator, "https://jsonplaceholder.typicode.com");
defer client.deinit();
std.debug.print("=== Testando API ===\n\n", .{});
// GET post por ID
std.debug.print("📥 Buscando post #1...\n", .{});
const post = try client.getPost(1);
defer {
allocator.free(post.title);
allocator.free(post.body);
}
std.debug.print("✅ Post encontrado:\n", .{});
std.debug.print(" Título: {s}\n", .{post.title});
std.debug.print(" User ID: {d}\n\n", .{post.userId});
// POST cria novo post
std.debug.print("📤 Criando novo post...\n", .{});
const new_post = try client.createPost(.{
.userId = 1,
.title = "Meu Post em Zig",
.body = "Este post foi criado usando Zig e JSON!",
});
defer {
allocator.free(new_post.title);
allocator.free(new_post.body);
}
std.debug.print("✅ Post criado com ID: {d}\n", .{new_post.id});
std.debug.print(" Título: {s}\n", .{new_post.title});
}
build.zig para o Cliente
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "api-client",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
const run_step = b.step("run", "Run API client");
run_step.dependOn(&run_cmd.step);
}
Parte 5: Tratamento de Erros
5.1 Erros Comuns de Parsing
const std = @import("std");
const User = struct {
id: u32,
name: []const u8,
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// JSON inválido
const invalid_json =
\\{
\\ "id": "not_a_number",
\\ "name": "João"
\\}
;
// Tenta parsear e trata erro
const result = std.json.parseFromSlice(
User,
allocator,
invalid_json,
.{},
);
if (result) |parsed| {
defer parsed.deinit();
std.debug.print("✅ Sucesso: {s}\n", .{parsed.value.name});
} else |err| {
std.debug.print("❌ Erro ao parsear JSON: {}\n", .{err});
// Erro esperado: error.InvalidCharacter (id deveria ser número)
}
}
5.2 Validação de Dados
const std = @import("std");
const CreateUserRequest = struct {
email: []const u8,
age: u32,
pub fn validate(self: CreateUserRequest) !void {
// Valida email
if (std.mem.indexOf(u8, self.email, "@") == null) {
return error.InvalidEmail;
}
// Valida idade
if (self.age < 13 or self.age > 120) {
return error.InvalidAge;
}
}
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const json_str =
\\{
\\ "email": "joao@exemplo.com",
\\ "age": 25
\\}
;
const parsed = try std.json.parseFromSlice(
CreateUserRequest,
allocator,
json_str,
.{},
);
defer parsed.deinit();
// Valida após parse
parsed.value.validate() catch |err| {
std.debug.print("❌ Validação falhou: {}\n", .{err});
return;
};
std.debug.print("✅ Dados válidos!\n", .{});
}
5.3 Estratégia de Fallback
const std = @import("std");
// Struct flexível que aceita variações
const ApiResponse = struct {
status: []const u8,
data: ?std.json.Value, // Opcional
error: ?[]const u8, // Opcional
message: ?[]const u8, // Opcional (fallback)
};
pub fn handleApiResponse(allocator: std.mem.Allocator, json_str: []const u8) !void {
const parsed = try std.json.parseFromSlice(
ApiResponse,
allocator,
json_str,
.{},
);
defer parsed.deinit();
const response = parsed.value;
if (std.mem.eql(u8, response.status, "error")) {
// Tenta várias fontes de mensagem de erro
const error_msg = response.error orelse
(if (response.message) |m| m else "Erro desconhecido");
std.debug.print("❌ Erro: {s}\n", .{error_msg});
return;
}
std.debug.print("✅ Sucesso!\n", .{});
if (response.data) |data| {
// Processa dados
std.debug.print("Dados recebidos: {}\n", .{data});
}
}
Parte 6: Casos Avançados
6.1 JSON com Unions (Tipos Variados)
const std = @import("std");
// Union para representar valor que pode ser string ou número
const StringOrNumber = union(enum) {
string: []const u8,
number: f64,
};
const FlexibleValue = struct {
id: u32,
value: StringOrNumber,
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// JSON onde "value" pode ser string ou número
const json_str =
\\[
\\ {"id": 1, "value": "texto"},
\\ {"id": 2, "value": 42.5}
\\]
;
const parsed = try std.json.parseFromSlice(
[]const FlexibleValue,
allocator,
json_str,
.{},
);
defer parsed.deinit();
for (parsed.value) |item| {
std.debug.print("ID: {d}, Value: ", .{item.id});
switch (item.value) {
.string => |s| std.debug.print("(string) {s}\n", .{s}),
.number => |n| std.debug.print("(number) {d}\n", .{n}),
}
}
}
6.2 Parse Parcial de JSON
const std = @import("std");
// Parse apenas os campos que interessam
const PartialUser = struct {
id: u32,
name: []const u8,
// Ignora: email, phone, website, company, address...
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// JSON grande com muitos campos
const large_json =
\\{
\\ "id": 1,
\\ "name": "João Silva",
\\ "username": "joaosilva",
\\ "email": "joao@exemplo.com",
\\ "address": {
\\ "street": "Rua Principal",
\\ "city": "São Paulo"
\\ },
\\ "phone": "11-99999-9999",
\\ "website": "joao.dev",
\\ "company": {
\\ "name": "Tech Corp",
\\ "catchPhrase": "Inovação sempre"
\\ }
\\}
;
// Parse só id e name — outros campos são ignorados
const parsed = try std.json.parseFromSlice(
PartialUser,
allocator,
large_json,
.{},
);
defer parsed.deinit();
std.debug.print("ID: {d}, Nome: {s}\n", .{
parsed.value.id,
parsed.value.name,
});
}
6.3 Ignorar Campos Desconhecidos
const std = @import("std");
const Config = struct {
name: []const u8,
version: []const u8,
// Ignora campos extras no JSON
pub const __json_options = .{
.ignore_unknown_fields = true,
};
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// JSON com campos extras que serão ignorados
const json_str =
\\{
\\ "name": "meu-app",
\\ "version": "1.0.0",
\\ "unknown_field_1": "valor",
\\ "unknown_field_2": 123,
\\ "future_feature": true
\\}
;
const parsed = try std.json.parseFromSlice(
Config,
allocator,
json_str,
.{ .ignore_unknown_fields = true },
);
defer parsed.deinit();
std.debug.print("✅ Parse bem-sucedido ignorando campos extras\n", .{});
std.debug.print("Name: {s}, Version: {s}\n", .{
parsed.value.name,
parsed.value.version,
});
}
Resumo e Checklist
Você aprendeu a trabalhar com JSON em Zig:
✅ Parse básico — Structs ↔ JSON
✅ Arrays e campos opcionais — []T e ?T
✅ Serialização — Struct → JSON formatado
✅ JSON dinâmico — std.json.Value para estruturas desconhecidas
✅ Cliente de API — Requisições HTTP com JSON
✅ Tratamento de erros — Validação e fallback
✅ Casos avançados — Unions, parse parcial, campos ignorados
Tabela Rápida de Referência
| Operação | Função | Exemplo |
|---|---|---|
| Parse para struct | parseFromSlice(T, allocator, json, .{}) | const user = parsed.value; |
| Serializar struct | stringifyAlloc(allocator, value, .{}) | const json = try std.json.stringifyAlloc(...); |
| JSON dinâmico | parseFromSlice(std.json.Value, ...) | root.object.get("key").?.string |
| Pretty print | .{ .whitespace = .indent_2 } | JSON com indentação |
| Campo opcional | ?T na struct | bio: ?[]const u8 |
| Ignorar campos | .ignore_unknown_fields = true | Usar em parseOptions |
Próximos Passos
Aprofunde seus conhecimentos:
- 🌐 Como Criar um Servidor HTTP em Zig — Crie APIs REST que usam JSON
- 🛠️ Como Criar uma CLI em Zig — Ferramentas de linha de comando com configuração JSON
- 🧪 Testes em Zig: Guia Completo — Teste seu parsing de JSON
- 📦 Tratamento de Erros em Zig — Error handling avançado
Exercícios Práticos
- Parse de API pública: Escreva um programa que consome a API do GitHub e lista repositórios de um usuário
- Configuração JSON: Crie um sistema de configuração que lê e escreve settings.json
- Validador de JSON: Implemente um programa que valida se um arquivo JSON está bem formado
- Conversor CSV→JSON: Converta arquivos CSV para formato JSON
Recursos Adicionais
- Documentação std.json
- Ziglearn - Chapter 5
- JSONPlaceholder — API gratuita para testes
Gostou deste tutorial? Compartilhe com a comunidade Zig! Tem dúvidas ou sugestões? Entre em contato conosco.
Última atualização: 10 de fevereiro de 2026
Versão do Zig: 0.13.0