fork of gitlab.com/devnw/zig/json for zig 0.17
devnw-json docs EXAMPLES.md
21 kB · 923 lines
Markdown
at main

Examples #

Practical examples demonstrating zig-json usage patterns.

Table of Contents #


Basic Parsing #

Parse a JSON String #

const std = @import("std");
const json = @import("json");

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

    const input =
        \\{
        \\  "name": "Alice",
        \\  "age": 30,
        \\  "emails": ["alice@example.com", "alice@work.com"]
        \\}
    ;

    var err = json.ParseError.init();
    var doc = try json.parse(allocator, input, .{}, &err);
    defer doc.deinit();

    // Access the root value
    std.debug.print("Parsed JSON with {d} members\n", .{doc.root.obj.len});
}

Parse with Custom Options #

var doc = try json.parse(allocator, input, .{
    .max_bytes = 1024 * 1024,      // 1MB limit
    .max_depth = 32,               // Max nesting depth
    .unique_keys = true,           // Reject duplicate keys
    .allow_bom = false,            // Reject UTF-8 BOM
}, &err);

Encoding Zig Types #

Encode a Struct #

const Point = struct {
    x: i32,
    y: i32,
    label: ?[]const u8 = null,
};

const p = Point{ .x = 100, .y = 200, .label = "origin" };

const output = try json.encode(allocator, p, .{});
defer allocator.free(output);
// {"x":100,"y":200,"label":"origin"}

Field Configuration #

const Person = struct {
    first_name: []const u8,
    last_name: []const u8,
    internal_id: u64,
    ssn: []const u8,

    pub const json_config = .{
        .first_name = json.FieldConfig{ .name = "firstName" },
        .last_name = json.FieldConfig{ .name = "lastName" },
        .internal_id = json.FieldConfig{ .skip = true },
        .ssn = json.FieldConfig{ .omitempty = true },
    };
};

const person = Person{
    .first_name = "John",
    .last_name = "Doe",
    .internal_id = 12345,
    .ssn = "",
};

const output = try json.encode(allocator, person, .{});
// {"firstName":"John","lastName":"Doe"}

Binary Data Encoding #

const Document = struct {
    name: []const u8,
    data: []const u8,

    pub const json_config = .{
        .data = json.FieldConfig{ .binary_format = .base64 },
    };
};

const doc = Document{
    .name = "secret.pdf",
    .data = "\x00\x01\x02\x03\x04",
};

const output = try json.encode(allocator, doc, .{});
// {"name":"secret.pdf","data":"AAECAwQ="}

Custom Marshaler #

const Point3D = struct {
    x: f64,
    y: f64,
    z: f64,

    pub fn jsonMarshal(self: @This(), alloc: std.mem.Allocator) !json.Value {
        var arr = try alloc.alloc(json.Value, 3);
        arr[0] = try json.toValue(alloc, self.x, .{});
        arr[1] = try json.toValue(alloc, self.y, .{});
        arr[2] = try json.toValue(alloc, self.z, .{});
        return .{ .arr = arr };
    }
};

const p = Point3D{ .x = 1.0, .y = 2.0, .z = 3.0 };
const output = try json.encode(allocator, p, .{ .allocator = allocator });
// [1,2,3]

Unmarshaling to Zig Types #

Basic Unmarshaling #

const Person = struct {
    name: []const u8,
    age: ?i32 = null,
    tags: []const []const u8 = &.{},
};

const input = "{\"name\": \"Alice\", \"age\": 30, \"tags\": [\"admin\", \"user\"]}";

var p = try json.unmarshal(Person, allocator, input);
defer json.free(Person, allocator, &p);

std.debug.print("Name: {s}, Age: {d}\n", .{p.name, p.age.?});
for (p.tags) |tag| {
    std.debug.print("  Tag: {s}\n", .{tag});
}

Unmarshal with Error Info #

const result = json.unmarshalWithInfo(Person, allocator, input);
switch (result) {
    .ok => |value| {
        defer json.free(Person, allocator, &value);
        // Use value
    },
    .err => |info| {
        std.debug.print("Error at {s}: {s}\n", .{info.path, info.message});
        std.debug.print("Expected: {s}, Found: {s}\n", .{
            info.expected orelse "N/A",
            info.found orelse "N/A",
        });
    },
}

Custom Unmarshaler #

const Point3D = struct {
    x: f64,
    y: f64,
    z: f64,

    pub fn jsonUnmarshal(alloc: std.mem.Allocator, value: json.Value) !Point3D {
        _ = alloc;
        if (value != .arr or value.arr.len != 3) return error.InvalidFormat;
        return .{
            .x = std.fmt.parseFloat(f64, value.arr[0].num.lexeme) catch return error.InvalidFormat,
            .y = std.fmt.parseFloat(f64, value.arr[1].num.lexeme) catch return error.InvalidFormat,
            .z = std.fmt.parseFloat(f64, value.arr[2].num.lexeme) catch return error.InvalidFormat,
        };
    }
};

const p = try json.unmarshal(Point3D, allocator, "[1.5, 2.5, 3.5]");
// p.x = 1.5, p.y = 2.5, p.z = 3.5

Time and Duration Types #

RFC 3339 Timestamps #

const Event = struct {
    name: []const u8,
    timestamp: json.Timestamp,
};

// Encode
const event = Event{
    .name = "deployment",
    .timestamp = json.Timestamp.fromUnix(1705314600),
};
const output = try json.encode(allocator, event, .{});
// {"name":"deployment","timestamp":"2024-01-15T10:30:00Z"}

// Unmarshal
const parsed = try json.unmarshal(Event, allocator, output);
std.debug.print("Unix time: {d}\n", .{parsed.timestamp.toUnix()});

Unix Timestamps #

const Response = struct {
    created_at: json.UnixTimestamp,
    updated_at: json.UnixTimestampMs,
};

const resp = Response{
    .created_at = .{ .seconds = 1705314600 },
    .updated_at = .{ .milliseconds = 1705314600123 },
};
const output = try json.encode(allocator, resp, .{});
// {"created_at":1705314600,"updated_at":1705314600123}

Custom Formatted Timestamps #

const SqlTimestamp = json.FormattedTimestamp(json.TimeFormat.sql_datetime);

const Record = struct {
    id: u64,
    created: SqlTimestamp,
};

const record = Record{
    .id = 1,
    .created = SqlTimestamp.fromUnix(1705314600),
};
const output = try json.encode(allocator, record, .{});
// {"id":1,"created":"2024-01-15 10:30:00"}

ISO 8601 Durations #

const Task = struct {
    name: []const u8,
    timeout: json.Duration,
};

const task = Task{
    .name = "backup",
    .timeout = json.Duration.fromSeconds(2 * 3600 + 30 * 60),
};
const output = try json.encode(allocator, task, .{});
// {"name":"backup","timeout":"PT2H30M"}

JSON Schema Validation #

Basic Validation #

const schema_json =
    \\{
    \\  "type": "object",
    \\  "properties": {
    \\    "name": {"type": "string", "minLength": 1},
    \\    "age": {"type": "integer", "minimum": 0, "maximum": 150}
    \\  },
    \\  "required": ["name", "age"]
    \\}
;

var err = json.ParseError.init();

// Parse and compile the schema
var schema_doc = try json.parse(allocator, schema_json, .{}, &err);
defer schema_doc.deinit();

var compiled = try json.compileSchema(allocator, &schema_doc, .{}, null, &err);
defer compiled.deinit();

// Validate
var instance_doc = try json.parse(allocator, "{\"name\": \"Alice\", \"age\": 30}", .{}, &err);
defer instance_doc.deinit();

const report = try json.validate(allocator, &compiled, instance_doc.root, .{});
defer allocator.free(report.errors);

if (report.ok) {
    std.debug.print("Valid!\n", .{});
}

Handling Validation Errors #

if (!report.ok) {
    for (report.errors) |e| {
        std.debug.print("Error at {s}: {s} ({s})\n", .{
            e.instance_ptr,
            e.message,
            e.keyword,
        });
    }
}

JSON Patch Operations #

Apply Patch #

const patch_mod = json.patch_mod;

var err = json.ParseError.init();
var doc = try json.parse(allocator, "{\"name\": \"Alice\", \"age\": 30}", .{}, &err);
defer doc.deinit();

const ops = [_]patch_mod.Operation{
    .{ .op = .replace, .path = "/age", .value = .{ .num = json.Number.fromInt(31) } },
    .{ .op = .add, .path = "/email", .value = .{ .str = "alice@example.com" } },
    .{ .op = .remove, .path = "/name" },
};

const result = try patch_mod.apply(allocator, doc.root, &ops);
defer patch_mod.freeValue(allocator, result);

const output = try json.stringify(allocator, result);
defer allocator.free(output);
// {"age":31,"email":"alice@example.com"}

Generate Diff Patch #

var from_doc = try json.parse(allocator, "{\"name\": \"Alice\"}", .{}, &err);
defer from_doc.deinit();

var to_doc = try json.parse(allocator, "{\"name\": \"Bob\", \"age\": 30}", .{}, &err);
defer to_doc.deinit();

const patch = try patch_mod.diff(allocator, from_doc.root, to_doc.root);
defer allocator.free(patch);

// patch contains: [{"op":"replace","path":"/name","value":"Bob"},{"op":"add","path":"/age","value":30}]

JSONPath Queries #

Basic Queries #

const input =
    \\{
    \\  "store": {
    \\    "books": [
    \\      {"title": "Book 1", "price": 10},
    \\      {"title": "Book 2", "price": 15},
    \\      {"title": "Book 3", "price": 20}
    \\    ]
    \\  }
    \\}
;

var err = json.ParseError.init();
var doc = try json.parse(allocator, input, .{}, &err);
defer doc.deinit();

// Get all book titles
var result = try json.jsonpath(allocator, doc.root, "$.store.books[*].title");
defer result.deinit();

for (result.values) |v| {
    std.debug.print("Title: {s}\n", .{v.str});
}

Filter Expressions #

// Books with price > 12
var result = try json.jsonpath(allocator, doc.root, "$.store.books[?(@.price > 12)]");
defer result.deinit();
// Returns Book 2 and Book 3

// Recursive descent - all prices anywhere in document
var all_prices = try json.jsonpath(allocator, doc.root, "$..price");
defer all_prices.deinit();

Union Expressions #

// Get first and third books
var result = try json.jsonpath(allocator, doc.root, "$.store.books[0,2]");
defer result.deinit();

// Get specific fields
var fields = try json.jsonpath(allocator, doc.root, "$.store.books[0]['title','price']");
defer fields.deinit();

JSON Lines (JSONL) #

Parse JSONL File #

const input =
    \\{"name": "Alice", "age": 30}
    \\{"name": "Bob", "age": 25}
    \\{"name": "Charlie", "age": 35}
;

var err_out = json.JsonlError.init();
var doc = try json.parseJsonl(allocator, input, .{}, &err_out);
defer doc.deinit();

// doc.root is an array of the parsed lines
for (doc.root.arr) |line| {
    const name = json.pointer_mod.evaluate(line, "/name");
    if (name) |n| std.debug.print("Name: {s}\n", .{n.str});
}

Stream JSONL #

var iter = json.jsonlIterator(allocator, input, .{});
defer iter.deinit();

while (try iter.next()) |value| {
    defer iter.freeValue(value);

    // Process each line individually
    const output = try json.stringify(allocator, value);
    defer allocator.free(output);
    std.debug.print("Line: {s}\n", .{output});
}

Schema Generation #

Generate from Type #

const Person = struct {
    name: []const u8,
    age: ?i32,
    email: ?[]const u8 = null,
    tags: []const []const u8 = &.{},
};

const schema = try json.generateSchema(Person, allocator, .{});
defer json.freeSchema(allocator, schema);

const output = try json.stringify(allocator, schema);
defer allocator.free(output);
// {
//   "type": "object",
//   "properties": {
//     "name": {"type": "string"},
//     "age": {"type": ["integer", "null"]},
//     "email": {"type": ["string", "null"]},
//     "tags": {"type": "array", "items": {"type": "string"}}
//   },
//   "required": ["name"]
// }

Streaming Tokenizer #

Low-Level Tokenization #

const input = "{\"name\": \"Alice\", \"values\": [1, 2, 3]}";

var tokenizer = json.Tokenizer.init(allocator, input, .{});
defer tokenizer.deinit();

while (true) {
    const token = tokenizer.next() catch |err| {
        std.debug.print("Error: {}\n", .{err});
        break;
    };

    if (token.type == .eof) break;

    switch (token.type) {
        .object_start => std.debug.print("Object start\n", .{}),
        .object_end => std.debug.print("Object end\n", .{}),
        .array_start => std.debug.print("Array start\n", .{}),
        .array_end => std.debug.print("Array end\n", .{}),
        .string => std.debug.print("String: {s}\n", .{token.bytes}),
        .number => std.debug.print("Number: {s}\n", .{token.bytes}),
        else => {},
    }
}

Read from File #

var file = try std.fs.cwd().openFile("data.json", .{});
defer file.close();

var tokenizer = json.readerTokenizer(allocator, file.reader(), .{});
defer tokenizer.deinit();

while (try tokenizer.next()) |token| {
    // Process tokens
}

Async Incremental Parsing #

Feed Chunks Incrementally #

var parser = json.AsyncParser.init(allocator, .{});
defer parser.deinit();

// Simulate receiving data in chunks
const chunks = [_][]const u8{
    "{\"name\":",
    " \"Alice\",",
    " \"age\": 30}",
};

for (chunks) |chunk| {
    try parser.feed(chunk);
}

if (try parser.finish()) |doc| {
    defer doc.deinit();
    std.debug.print("Parsed complete document\n", .{});
}

Error Recovery #

var parser = json.AsyncParser.init(allocator, .{
    .recovery_mode = .skip_value,
});
defer parser.deinit();

// Feed potentially malformed JSON
try parser.feed("[1, invalid, 3]");

if (try parser.finish()) |doc| {
    defer doc.deinit();
    std.debug.print("Recovered from {d} errors\n", .{parser.errors_recovered});
}

SIMD Acceleration #

SIMD Parser #

var parser = json.SimdParser.init(allocator, .{});
defer parser.deinit();

const doc = try parser.parse(large_json_input);
defer doc.deinit();

Feature Detection #

const features = json.simd_mod.detectFeatures();

std.debug.print("SIMD Support: {s}\n", .{features.describe()});
std.debug.print("  SSE4.2: {}\n", .{features.has_sse42});
std.debug.print("  AVX2: {}\n", .{features.has_avx2});
std.debug.print("  AVX-512F: {}\n", .{features.has_avx512f});
std.debug.print("  NEON: {}\n", .{features.has_neon});
std.debug.print("  Recommended width: {d}\n", .{features.recommended_width});

CBOR Binary Encoding #

JSON to CBOR #

var err = json.ParseError.init();
var doc = try json.parse(allocator, "{\"name\": \"Alice\", \"age\": 30}", .{}, &err);
defer doc.deinit();

const cbor_bytes = try json.cborEncode(allocator, doc.root, .{});
defer allocator.free(cbor_bytes);

std.debug.print("CBOR size: {d} bytes\n", .{cbor_bytes.len});

CBOR to JSON #

const decoded = try json.cborDecode(allocator, cbor_bytes, .{});
defer json.cborFree(allocator, decoded);

const json_output = try json.stringify(allocator, decoded);
defer allocator.free(json_output);

Streaming CBOR Decoder #

var decoder = json.cbor_mod.StreamingDecoder.init(allocator);
defer decoder.deinit();

// Feed CBOR data incrementally
for (chunks) |chunk| {
    try decoder.feed(chunk);

    while (true) {
        const result = decoder.decodeNext();
        switch (result) {
            .value => |v| {
                // Complete value decoded
                const output = try json.stringify(allocator, v);
                defer allocator.free(output);
                std.debug.print("Decoded: {s}\n", .{output});
            },
            .need_data => break,
            .done => break,
            .err => |e| return e,
        }
    }
}

CBOR Diagnostic Notation #

const diagnostic = try json.cborToDiagnostic(allocator, cbor_bytes);
defer allocator.free(diagnostic);
// {"name": "Alice", "age": 30}

JSON-LD Processing #

Expand JSON-LD #

const input =
    \\{
    \\  "@context": {
    \\    "name": "http://schema.org/name",
    \\    "Person": "http://schema.org/Person"
    \\  },
    \\  "@type": "Person",
    \\  "name": "Alice"
    \\}
;

var err = json.ParseError.init();
var doc = try json.parse(allocator, input, .{}, &err);
defer doc.deinit();

const expanded = try json.ldExpand(allocator, doc.root, .{});
defer json.ldFree(allocator, expanded);

const output = try json.stringify(allocator, expanded);
defer allocator.free(output);
// [{"@type":["http://schema.org/Person"],"http://schema.org/name":[{"@value":"Alice"}]}]

Compact with Context #

const context =
    \\{
    \\  "name": "http://schema.org/name",
    \\  "Person": "http://schema.org/Person"
    \\}
;

var ctx_doc = try json.parse(allocator, context, .{}, &err);
defer ctx_doc.deinit();

const compacted = try json.ldCompact(allocator, expanded, ctx_doc.root, .{});
defer json.ldFree(allocator, compacted);

Remote Context Loading #

var loader = json.jsonld_mod.RemoteContextLoader.init(allocator);
defer loader.deinit();

// Preload common contexts
try loader.preloadContext("https://schema.org/", schema_org_context);
try loader.preloadContext("https://w3id.org/security/v1", security_context);

// Use in JSON-LD processing
const options = json.LdOptions{
    .context_loader = &loader,
};

const expanded = try json.ldExpand(allocator, doc.root, options);

Convert to N-Quads #

const nquads = try json.ldToNQuads(allocator, doc.root, .{});
defer allocator.free(nquads);
// <http://example.org/alice> <http://schema.org/name> "Alice" .

Working with Values #

var doc = try json.parse(allocator, input, .{}, &err);
defer doc.deinit();

if (doc.root == .obj) {
    for (doc.root.obj) |member| {
        std.debug.print("Key: {s}\n", .{member.k});

        switch (member.v) {
            .str => |s| std.debug.print("  String: {s}\n", .{s}),
            .num => |n| std.debug.print("  Number: {s}\n", .{n.lexeme}),
            .bool => |b| std.debug.print("  Bool: {}\n", .{b}),
            .null => std.debug.print("  Null\n", .{}),
            .arr => |a| std.debug.print("  Array with {d} items\n", .{a.len}),
            .obj => |o| std.debug.print("  Object with {d} members\n", .{o.len}),
        }
    }
}

Use JSON Pointer #

const pointer = json.pointer_mod;

// Get nested value
if (pointer.evaluate(doc.root, "/users/0/email")) |value| {
    if (value == .str) {
        std.debug.print("Email: {s}\n", .{value.str});
    }
}

Compare Values #

var doc1 = try json.parse(allocator, "{\"a\":1,\"b\":2}", .{}, &err);
defer doc1.deinit();

var doc2 = try json.parse(allocator, "{\"b\":2,\"a\":1}", .{}, &err);
defer doc2.deinit();

// Objects with same content are equal regardless of key order
if (doc1.root.eql(doc2.root)) {
    std.debug.print("Values are equal\n", .{});
}

Pretty Printing #

Using Preset Options #

const data = .{ .x = 1, .y = 2, .nested = .{ .a = "hello" } };

// 2-space indentation
const pretty2 = try json.encode(allocator, data, json.encode_mod.pretty_2);

// 4-space indentation
const pretty4 = try json.encode(allocator, data, json.encode_mod.pretty_4);

// Tab indentation
const prettyTab = try json.encode(allocator, data, json.encode_mod.pretty_tab);

Custom Indentation #

const output = try json.encode(allocator, data, .{
    .indent = "    ",  // 4 spaces
});

Error Handling #

Parse Errors #

var err = json.ParseError.init();
const result = json.parse(allocator, "invalid json", .{}, &err);

if (result) |doc| {
    defer doc.deinit();
    // Success
} else |e| {
    std.debug.print("Parse error at line {d}, column {d}: {s}\n", .{
        err.line,
        err.col,
        @tagName(err.code),
    });
}

Common Error Codes #

switch (err.code) {
    .UnexpectedEof => std.debug.print("Unexpected end of input\n", .{}),
    .UnexpectedToken => std.debug.print("Unexpected character\n", .{}),
    .InvalidUtf8 => std.debug.print("Invalid UTF-8 sequence\n", .{}),
    .InvalidNumber => std.debug.print("Invalid number format\n", .{}),
    .DuplicateKey => std.debug.print("Duplicate object key\n", .{}),
    .DepthLimit => std.debug.print("Nesting too deep\n", .{}),
    else => std.debug.print("Error: {s}\n", .{@tagName(err.code)}),
}

Streaming Output #

Write to File #

var file = try std.fs.cwd().createFile("output.json", .{});
defer file.close();

const data = .{ .key = "value", .numbers = [_]i32{ 1, 2, 3 } };
try json.encodeToWriter(file.writer(), data, .{ .indent = "  " });

Write to ArrayList #

var list = std.ArrayList(u8).init(allocator);
defer list.deinit();

try json.encodeToWriter(list.writer(), data, .{});
const output = list.items;

Write to stdout #

const stdout = std.io.getStdOut().writer();
try json.encodeToWriter(stdout, data, .{ .indent = "  " });
try stdout.writeByte('\n');