Read and write JSON
Json().encode and Json().decode<T> work on any Encode/Decode type for free; an option of the format spells the field names, and the pair is written by hand only where a constructor cannot say what a document may.
Json reads and writes any type that implements Encode/Decode, and both are generated for an ordinary type - there
is nothing to write for the common case. Json is a value with the options of the format; writing the pair by hand is
for the type whose constructor cannot express what a document may say, the same reason parse exists next to a plain
constructor.
Steps#
Encode and decode an ordinary type with nothing written for it. Both are generated whenever the constructor is usable from outside and every parameter has them.
const json = Json() const text = json.encode(User(name: "Ada", age: 36)) const user = json.decode<User>(text)?Match on
decode'sResultinstead of using?where malformed input is expected, not a bug. Both a syntax error and a document that does not fit the type answer the sameJsonError, and one of the second kind names the field chain it went wrong in (price.currency: a value is needed).match json.decode<User>(text) { Ok(user) => print user.name Fail(error) => print "rejected: {error}" }Change how every field name is spelled with an option of the format, not with a word on the type.
Json(naming: .SnakeCase)writes and readslogLabelaslog_label, andJson(strict: true)rejects a field of the input that no type asked for.const snake = Json(naming: .SnakeCase) print snake.encode(setting)Write
EncodeandDecodeby hand only where aprivatefield keeps the generated ones from working, because a private field with no default makes the constructor unusable from outside.encodewrites through theEncoder's methods, named after the built-in types;decodeis astatic fnthat reads through theDecoderand answers aResult. Both take the format as a type parameter.extend Percent with Encode { fn encode<Target: Encoder>(var target: Target) { target.int value } } extend Percent with Decode { static fn decode<Source: Decoder>(var source: Source): Result<Percent, DecodeError> { const raw = source.int()? if raw < 0 || raw > 100 { return Fail DecodeError("{raw} is not a percent") } Ok Self(raw) } }Reach for
JsonValueonly for a document whose shape is not known ahead of time.json.parseanswers one without needing a type at all; use it for a passthrough field or a preview, not for data your program already has a type for.
Pitfalls#
decodefails on well-formed JSON that does not fit the type, not only on malformed text.isSyntaxError()on theJsonErrortells the two apart;cause()gives theDecodeErrororUtf8Errorunderneath either way.- A field that is a
typeneeds no extra work, as long as that type is itselfEncode/Decode- nesting is where the generated pair is most of the benefit, because a hand-written encoder would otherwise have to be written for every level. - A field with a default may be missing from the document, and takes its default; one without a default has to be
there. A
privatefield with a default is no parameter of the constructor from outside, so it is never written and never read. - Writing
EncodewithoutDecodeis legal and common. A type your program only ever sends outward has no reason to parse itself back. Decode.decodeis astatic fn, not a method. It takes theDecoderand answersSelf; there is noselfto call it on before a value exists.
Full example#
type Percent {
private value: Int
static fn of(value: Int): Percent? {
if value < 0 || value > 100 {
return None
}
Some Self(value)
}
}
extend Percent with Encode {
fn encode<Target: Encoder>(var target: Target) {
target.int value
}
}
extend Percent with Decode {
static fn decode<Source: Decoder>(var source: Source): Result<Percent, DecodeError> {
const raw = source.int()?
if raw < 0 || raw > 100 {
return Fail DecodeError("{raw} is not a percent")
}
Ok Self(raw)
}
}
const json = Json()
const text = json.encode(Percent.of(42)?)
print text
match json.decode<Percent>(text) {
Ok(percent) => print percent
Fail(error) => print "rejected: {error}"
}Related#
- std/json -
Json,JsonValueandJsonErrorin full. - std/encoding -
Encode,Decode,EncoderandDecoder, whichJsonimplements. - Parse text into a type - the same private-field-plus-factory shape, for text instead of JSON.