JSON and time
There and back#
Task. Turn a data structure into JSON and read it back.
use std.ser { toJson, fromJson }
enum Role { Admin; Member; Guest }
data Address { city: str; zip: str }
data User {
id: i64
name: str
roles: List<Role>
address: Address
nickname: str? // an optional field: none becomes null
tags: Map<str, str>
}
fn main() io {
let u = User{
id: 7, name: "Ann", roles: [Role.Admin, Role.Member],
address: Address{ city: "Paris", zip: "75001" },
nickname: none, tags: {"team": "core"},
}
let text = toJson(&u)! // any Serializable type serializes without extra code
print(text)
let back: User = fromJson(&text)!
print("{back.name} {back.address.city} {back.roles.len()} {back.tags.get("team") ?? "-"}")
}Output
{"id":"7","name":"Ann","roles":["Admin","Member"],"address":{"city":"Paris","zip":"75001"},"tags":{"team":"core"}}
Ann Paris 2 core- A type is serializable automatically if all its fields are:
data,enum,List,Map,T?, nested records. No annotations or code are needed. toJson(&u)returnsstr | EncodeError,fromJson(&text)returnsT | DecodeError; the result type is set by the annotationlet back: User = …or by an explicitfromJson<User>(…).- An empty
nickname: str?is simply absent from the JSON. 64-bit integers are written as strings ("id":"7") - this keeps JSON safe for JavaScript, whose numbers aref64. Reading accepts both forms. - The same code works with the Msgpack and Cbor formats (
std.ser): the format is a parameter, not a separate library.
Decoding errors#
Task. Accept data from an untrusted source and not crash.
use std.ser { fromJson }
data Point { x: i64; y: i64 }
fn parse(text: str) -> str {
match fromJson<Point>(&text) {
Point p => "ok {p.x},{p.y}"
DecodeError => "decode error" // DecodeError is an ordinary error value
}
}
fn main() io {
print(parse("{{\"x\":1,\"y\":2}}"))
print(parse("{{\"x\":1}}")) // field y is missing
print(parse("{{\"x\":\"one\",\"y\":2}}")) // wrong type
print(parse("not json"))
}Output
ok 1,2 decode error decode error decode error
- Any problem - a missing field, a wrong type, a syntax error - gives a
DecodeError; the program does not panic. - Refined types in fields (
port: Portwithwhere 1..=65535) are checked automatically on decoding (§9.4), so "validation" often comes down to choosing the right type. - In an HTTP handler (recipe) a
DecodeErrorbecomes a 400 response.
Dates and durations#
Task. Add durations, count calendar intervals and not get confused by leap years.
use std.time { Date }
fn main() io {
let timeout = 90s + 500ms // Duration literals: ms, s, m, h, d
print("{timeout.millis()} ms = {timeout.seconds()} s")
print("{(2h + 30m).minutes()} minutes")
let d = Date.of(2024, 2, 28)!
print("{d.toIso()} {d.weekday()} leap: {d.isLeapYear()}")
print("{d.plusDays(2).toIso()} {(d + 1.month()).toIso()}")
let other = Date.of(2025, 1, 1)!
print("{d.daysUntil(other)} days until {other.toIso()}")
match Date.of(2023, 2, 30) {
Date x => print("valid {x.toIso()}")
InvalidValue => print("no such date")
}
match Duration.parse("1h30m") {
Duration x => print("{x.minutes()}")
ParseError => print("bad duration")
}
}Output
90500 ms = 90 s 150 minutes 2024-02-28 Wed leap: true 2024-03-01 2024-03-28 308 days until 2025-01-01 no such date 90
- The literals
90s,500ms,2h,30mhave the typeDuration; they can be added and compared. Units cannot be mixed up becauseDurationis a separate type, not a number. Date.of(2023, 2, 30)returnsDate | InvalidValue: a nonexistent date cannot be created.d + 1.month()is calendar arithmetic (Period), whileplusDays(n)shifts by days;Durationis exact time (seconds),Periodis calendar (months, years).- There are
TzandZonedTimefor time zones;std.time.now()reads the clock and needs thetimeeffect, so pure functions receive the time as an argument (and are easily tested with virtual clocks, recipe).
Back: cookbook.