DocsStandard library

JSON.

Decode directly into typed data or use a generic document when its shape is unknown.

The best place to start is a declared record with derived codecs. That way you parse text straight into your own type, without building a generic JSON tree first.

API reference: Json

Typed values.

import Json exposing (Decode, Encode(..))

type Person = { name : String, age : Int } deriving (Encode, Decode, Show)

main() =
    print (Json.parse @Person "{\"name\":\"Ada\",\"age\":4}")
    print (Json.stringify (Person { name = "Ada", age = 4 }))

The first result is Ok (Person { name = "Ada", age = 4 }), and the second is an Ok holding the encoded JSON string. Both operations report errors as values.

Record keys can arrive in any order, and unknown keys are skipped. A missing required key, or a declared key that appears twice, is an error. Records encode in declaration order. Lists become arrays, Unit and Nothing become null, and Just uses the representation of its payload.

Union constructors without fields encode as strings, and constructors with fields encode as single-key objects holding arrays. Use this derived representation only when it matches your protocol.

Type witnesses.

@Person picks the decoder through a closed type witness. For a parameterized type, write @(List Person). You need the witness because the result alone doesn’t tell Fango which decoder to use.

Field options.

Attributes let a declared record differ from its wire format. Here name uses a different key, and retries has a fallback:

import Json exposing (Decode, Encode(..))

type Settings =
    { name : String #[Json.Key "full_name"]
    , retries : Int #[Json.Default `3`]
    }
    deriving (Encode, Decode, Show)

main() =
    print (Json.parse @Settings "{\"full_name\":\"Ada\"}")
    print (Json.stringify (Settings { name = "Ada", retries = 2 }))
Ok (Settings { name = "Ada", retries = 3 })
Ok "{\"full_name\":\"Ada\",\"retries\":2}"

Json.Key changes the key that’s written and the key the decoder accepts. Json.Default holds quoted code that runs only when the field is absent. Json.Skip leaves a field out of the document, and it needs a default so decoding can still construct the record.

Each option can appear once per field, and the keys that get emitted must be unique. The JSON derivers check these choices at compile time. They won’t turn a present value of the wrong type into a default.

Value tree.

When you don’t know the shape in advance, parse a generic document and look at its constructors:

import Json
import Result exposing (Result(..))

main() =
    case Json.parse @Json.Value "{\"name\":\"Ada\",\"score\":4}" of
        Ok (Json.Object fields) -> print (List.map { (key, _) -> key } fields)
        Ok _ -> print "expected an object"
        Err error -> print error.message
["name", "score"]

Generic values tell apart null, booleans, numbers, text, arrays, and ordered object members. Numbers keep their original text rather than being forced into a Float just to build the tree.

A tree is handy for inspection or for dynamic structure. For a known protocol, though, a typed decoder usually makes the application code simpler and catches shape errors right at the input boundary.

Text adapters and custom encoders.

parseBytes accepts byte data, and read @Type reader decodes incrementally from a byte reader. write writer value emits to a byte writer. The byte entry points use UTF-8, while the text ones use the encoding of your text adapter.

For an incremental producer, install an emission handler and write a stream as an array:

import Bytes
import Json
import Stream
import Writer

numbers : List Int
numbers = [1, 2, 3]

main() =
    (result, bytes) = Writer.collecting { output ->
        Json.withWriter output { Json.array (Stream.fromList numbers) }
    }
    print result
    print (Bytes.toString bytes)
Ok ()
Just "[1,2,3]"

Json.array walks its input while emitting each value, instead of collecting a document tree first. Json.object likewise produces field values when it reaches them. Flushing is still up to whoever owns the byte writer.

A custom encoder implements Encode.encodeValue, using emission operations or existing encoders. For custom decoding, start with Json.Pull.

Errors.

A Json.Error records a message, a byte offset, a line, a byte-based column, and a path into the nested data. Malformed UTF-8, invalid escapes, trailing data, excessive nesting, and out-of-range numbers are all rejected.

If encoding fails, a prefix of the output may already have been written, because a result-returning writer isn’t an output transaction. When partial output matters, keep protocol framing and error handling outside the encoder.