/home/huy/everyday

everyday: Development Log

01.09.2022 - Zig/JSON in 5 minutes

Zig has built-in support for JSON via the std.json module.

To convert any object into a JSON string, we use std.json.stringify(<input>, <options>, <output stream>):

const x = Place{
  .lat = 51.997664,
  .long = -0.740687,
};

var buf: [100]u8 = undefined;
var fba = std.heap.FixedBufferAllocator.init(&buf);
var string = std.ArrayList(u8).init(fba.allocator());
try std.json.stringify(x, .{}, string.writer());

See StringifyOptions struct for more details about what you can pass into the options.

To parse a JSON string into an object, we can do it in two ways:

  • Parse into a pre-defined struct with std.json.parse(<output type>, <tokens>, <options>):

    const Foo = struct { a: i32, b: bool };
    const s =
        \\ {
        \\   "a": 15, "b": true
        \\ }
    ;
    const stream = std.json.TokenStream.init(s);
    const parsedData = try std.json.parse(Foo, &stream, .{});
    

    The TokenStream is a streaming parser that returns a stream of JSON, so we can pass them into std.json.parse.

    The JSON parser requires an allocator if the input JSON data contains strings or arrays, and we need to free them after use with std.json.parseFree:

    const gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer std.debug.assert(gpa.deinit());
    
    const Foo = struct { a: i32, b: bool, c: []u8 };
    const s =
        \\ {
        \\   "a": 15, "b": true,
        \\   "c": "hello world"
        \\ }
    ;
    const stream = std.json.TokenStream.init(s);
    const parsedData = try std.json.parse(Foo, &stream, .{
      .allocator = gpa.allocator()
    });
    defer std.json.parseFree(Foo, parsedData, .{
      .allocator = gpa.allocator()
    });
    

    Try comment out the defer std.json.parseFree statement to see how's the memory leak detection of GeneralPurposeAllocator works.

    Checkout ParseOptions struct for more about what options you can pass, for example:

    • Set ignore_unknown_fields to true will not return any error if there is a mismatch between the output type and input data.
    • Set duplicate_field_behavior will change the default's behavior when there is a duplicate field in your JSON input.
  • Parse into a dynamic object of type std.json.ValueTree with a non-stream parser std.json.Parser(<allocator>, <copy input string>):

    var parser = std.json.Parser.init(allocator, false);
    defer parser.deinit();
    
    const s =
        \\ {
        \\   "a": 15, "b": true,
        \\   "c": "hello world"
        \\ }
    ;
    
    var tree = try parser.parse(s);
    defer tree.deinit();
    
    // @TypeOf(tree.root) == std.json.Value
    
    // Access the fields value via .get() method
    var a = tree.root.Object.get("a").?;
    var b = tree.root.Object.get("b").?;
    var c = tree.root.Object.get("c").?;
    

    There is also a dump() method that available on any std.json.Value, to stringify an object and print it out to the stderr, best for debugging purpose:

    tree.root.dump();
    

For more details, you should read the implementation of std.json module, also, don't skip the tests at the bottom of the source file. They're super useful!

Read more