The JSON edge: bytes in through OTP's :json, iodata out from the staged program.
The two directions are not the same problem, and only one of them fuses.
Encoding does. Rupa.encode_json/3 walks the decoded value once and emits iodata, so the
wire map Rupa.encode/3 would have built is never built at all. Wire keys and constant values
are rendered to binaries while the schema is staged, so what is left at run time is escaping
the strings your value actually carries and consing the pieces together.
Decoding does not, and the reason is the callback API rather than anything about Rupa.
:json.decode/3 hands a value's key to object_push only after that value is already built,
so when a nested object starts, nothing in scope says which field it belongs to — and a field
is exactly where a schema differs from a parser. The accumulator threads down through
object_start and array_start, which is enough to direct the root and an array's elements,
and not enough for an object's fields. So Rupa.decode_json/3 is :json.decode/3 followed by
Rupa.decode/3, which is what the name promises and no more. bench/json.exs is the
measurement; a Rupa-owned parser is the way past it, and not in 0.1.0.
Nothing here is public API. Rupa.decode_json/3 and Rupa.encode_json/3 are.
The iodata shape
Both backends build an object or an array as a reversed list of entries, each one carrying its own leading comma, and the helper that closes the bracket drops the first comma again. That is how OTP's own encoder does it, and it is why a field costs one cons cell rather than a branch on whether it is the first.