Stringifying arbitrary json

I decided to try out zig and I’m starting with a simple exercise of reading some arbitrary JSON data, parsing it to a type, parsing back to a string, then saving it back again. I got roughly this far:

const json_input = try std.fs.cwd().readFileAlloc(allocator, "./test.json", 4096);

const parsed = try std.json.parseFromSlice(std.json.Value, allocator, json_input, .{});
    
var arr = std.ArrayList(u8).init(allocator);
    
try parsed.value.jsonStringify(arr.writer());

Loading and parsing to Zig’s built in dynamic json type works, I can query data and it prints successfully, but when going the other way I run into:

error: expected type '[]const u8', found '@TypeOf(null)'

I’m not sure what’s null and why it can’t write it to the string as "null’.

The json for context:

{
    "string": "hi",
    "num": 2,
    "array": [
        {
            "one": "a",
            "two": "b"
        },
        {
            "one": "c",
            "two": "d"
        }
    ]
}

Hey, welcome to Ziggit!

The problem here is that you’re using the wrong kind of writer.

Zig currently uses implicit interfaces for readers and writers which can be quite confusing because they’re labeled anytype anywhere they’re required as an argument, so basically there’s only the name of the parameter to tell you that a reader or a writer is needed here (there are plans to change that).

However std.json has a bunch of specialized readers and writers. This is as of now only indicated by the argument for std.json.Value.jsonStringify being called jws (JSON write stream) instead of writer. The compiler doesn’t complain as much as it probably should here if you use the wrong writer interface because both std.json.WriteStream and std.io.Writer (which is what arr.writer() returns) happen to have a write function, however std.io.Writer.write indeed only accepts []const u8 as a parameter, so if you try to .write(null) (which is supported by std.json.WriteStream.write) you will get an error.

This means that you have to wrap your std.ArrayList writer in a std.json.WriteStream first before you can supply it to std.json.Value.jsonStringify like this:

[...]
var arr = std.ArrayList(u8).init(allocator);
var jws = std.json.writeStream(arr.writer(), .{});
try parsed.value.jsonStringify(&jws);

EDIT: Or just use std.json.stringify(), see @stratts reply

3 Likes

Ah, interesting! I see what you mean looking at the WriteStream source.

.null => {
             try self.valueStart();
             try self.stream.writeAll("null");
             self.valueDone();
             return;
            },

Zig interfaces/generics are tricky, hard to know what they are looking for!

1 Like

You should also be able to just do:

try std.json.stringify(parsed.value, .{}, arr.writer());

jsonStringify is a function you can add to structs, unions, and enums to override the default serialisation behaviour. It’s defined on std.json.Value so it can output the string, number, etc, that it represents, instead of its internal representation.

This is then called by std.json.stringify() - you can call it directly, and get a valid JSON string out the other side, but as it’s designed to be used internally it expects an std.json.WriteStream as Justus2308 pointed out, instead of the standard writer.

3 Likes

Yeah, anytype is very convenient, but only once you know what kind of type is actually expected :slight_smile:
Generally though I find that the stdlib does a really good job at keeping to its own conventions (e.g. readers/writers) and if there are exceptions like WriteStream there‘s usually a convenient abstraction available that behaves in line with the rest of std (like std.json.stringify() as stratts has pointed out)

3 Likes

Oof, I feel dumb for not trying the external version. Too many years of OOP I guess, I habitually look for members to use first.