Skip to main content

std.data.json — JSON helpers

std.data.json (also exported as std.json) gives you explicit JSON parsing and conversion routines for working with Mux Json values.

  • data.json.parse(string source) -> result<Json, string> — parses a JSON string.
  • data.json.from_map(map<string, T>) -> result<Json, string> — converts a string-keyed map into a Json object (generic over values).
  • data.json.to_map(Json value) -> result<map<string, Json>, string> — extracts an object map if the Json value is an object.

Reading values out of a document

A Json value has typed accessors. Each returns a result<T, string>, and the error names what was actually there:

AccessorReturns
as_string()result<string, string>
as_int()result<int, string>
as_float()result<float, string>
as_bool()result<bool, string>
as_list()result<list<Json>, string>
as_map()result<map<string, Json>, string>
is_null()bool

They return a result rather than an optional because "not an int" is worth saying why. A bare none leaves you unable to tell a string from a null from something else - exactly the information you need when a document is not the shape you expected:

expected an int, found a string

That matters most in the escape hatch, where you are deliberately reading data whose shape you could not declare. For a document you can describe, prefer parsing straight into a class - the error there names the field as well.

Mux
Loading...

stringify is not an accessor

stringify returns the JSON encoding of a value, which for a string includes its quotes:

Mux
Loading...

Use stringify when you want JSON text back out. Use an accessor when you want the value.

Pick the accessor by what the field holds

An accessor reports the wrong kind rather than converting, so a number read with as_string is err("expected a string, found an int"), not "36". A status code in a document is a number, so it reads through as_int.

An integral float converts: {"n": 42.0} reads through as_int as 42. A fractional one does not - 1.5 is an error rather than silently truncating to 1 - and neither does a value outside the range of an int.

Round-trip fidelity

Parsing a document and serializing it again preserves its values and their order.

Integers stay integers. A whole number is not widened to a floating-point value, so {"id":42} comes back as {"id":42} and not {"id":42.0}. That matters wherever a receiver is strict about the difference - an HTTP status, a record id, an array index - and it means values beyond the range a float can represent exactly keep their value rather than silently rounding to a nearby one. A number written with a decimal point stays a float.

Key order is preserved. Object keys come back in the order the document listed them rather than sorted, so a re-serialized document reads the way it was written and diffs stay meaningful. This is the same guarantee Mux's own map gives: printed output does not depend on how a container arranged itself internally.

What is not preserved is source formatting. Insignificant whitespace, the choice between an escape and a literal character, and other spelling details of the original text are normalized by serialization. So a round trip is not byte-for-byte, and re-serialized output is not by itself a canonical form - signing or byte-comparing it requires a canonicalization step these routines do not provide.

Mux
Loading...