Getting started
A short walk through encoding a record to JSON and decoding it back.
Install
Add saga_json to your project.toml:
[dependencies]
saga_json = { git = "https://github.com/dylantf/saga_json" }Then saga install to fetch it.
Three layers
saga_json has three layers. Use whichever fits the task:
- The
JsonAST. Build values withstring,int,array,object. Serialize withrender. Parse withparse_string. Use this when the JSON shape doesn't map cleanly to a Saga type. - The
ToJsonandFromJsontraits. Write an impl by hand or usederiving. Composition is automatic: a record whose fields all have impls gets one too. - The
serializeanddeserializeone-shot helpers on top of the traits. Typed value to JSON string and back, in a single call.
Most code lives at layer 2 or 3.
Encode
The shortest encoder is deriving (ToJson) plus serialize:
import SagaJson.Encode as E (ToJson, serialize)
record Person {
name: String,
age: Int,
} deriving (ToJson)
main () = {
serialize (Person { name: "Alice", age: 30 })
# "{"name":"Alice","age":30}"
}deriving (ToJson) synthesizes the impl. serialize calls to_json
on the value and renders the result to a compact string.
Lists, Maybe, tuples (arity 2 through 10), and the primitive types
(String, Int, Float, Bool) all have impls out of the box.
Decode
The mirror image. Add FromJson to the deriving clause and call
deserialize:
import SagaJson as J
import SagaJson.Decode as D (FromJson, deserialize)
record Person {
name: String,
age: Int,
} deriving (FromJson)
main () = {
let input = """{"name":"Alice","age":30}"""
deserialize input : Result Person J.Error
# Ok(Person { name: "Alice", age: 30 })
}deserialize returns Result a Error. On success, Ok value. On
failure (missing field, wrong shape, parse error), Err e. The error
carries a description plus the path to where it went wrong.
The type annotation matters. deserialize returns a generic
Result a Error, so Saga needs the annotation to know what a is.
Round-trip
Combine the two and anything you encode comes back equal to what you
put in (modulo Float precision):
record Person {
name: String,
age: Int,
} deriving (Debug, Eq, ToJson, FromJson)
main () = {
let alice = Person { name: "Alice", age: 30 }
let json = serialize alice
let back = deserialize json : Result Person J.Error
back == Ok alice # True
}If you pass Options on one side, pass the same Options on the
other or the round-trip breaks. See the customization
guide for the cases where it does and doesn't
work.
ADTs
Sum types derive the same way:
type Role =
| Admin
| Editor
| Viewer
deriving (ToJson, FromJson)Unit variants encode as bare JSON strings ("Admin").
Payload-bearing variants encode as externally-tagged objects
({"Mention": "alice"}). This matches the serde default. The
decoder also accepts the legacy wrapped form ({"Admin": null}) for
backwards compatibility.
If you want every variant as a bare string regardless of payload, or
every variant wrapped regardless of arity, see the as_enum and
as_tagged strategies in the customization
guide.
What's next
- Encoding guide. Hand-written impls, the primitive
constructors, and
derive_with. - Decoding guide. Decoder combinators, error shapes, navigating nested JSON by hand.
- Customization guide.
Options, ADT tag formats, key renaming, strategy functions. - Post-processing guide.
update_field,rename_field,map_objectfor reshapingJsonafter the fact.