DocsStandard library
JSON pull parsing.
Write a custom decoder or consume a document without building its full tree.
Most programs should start with typed JSON. Reach for Json.Pull when you need custom decoding, want to consume only part of a document, or need control at the level of individual tokens.
API reference: Json.Pull
Pull handler.
Json.Pull.withReader reader action installs a token cursor over a byte reader and returns a Result Json.Error a. Here the action reads two tokens from a scalar document:
import Bytes
import Json.Pull as Pull
import Reader
main() =
result = Reader.withBytes (Bytes.fromString "true") { reader ->
Pull.withReader reader {
first = Pull.next()
last = Pull.next()
(first, last)
}
}
print result
Ok (BooleanToken True, EndInput)
first is the boolean token, and last is EndInput. next() consumes a token, while peek() looks ahead without consuming it. Pull.fail message reports an error at the current position and path. withTextReader uses the encoding of an existing text adapter.
While a pull scope is active, advance the input only through that scope. The underlying reader’s position can lag behind the buffered parsing, and when the scope exits, the pull handler reconciles what was consumed, including tokens scanned through lookahead.
Walk a container.
The container helpers take care of brackets and commas, so each value is ready for you to decode. This sums an array without building a list:
import Bytes
import Json.Pull as Pull
import Reader
sumArray : () ->{Pull.Pull} Int
sumArray() =
Pull.beginArray()
total = sumElements True 0
case Pull.next() of
Pull.EndInput -> total
_ -> Pull.fail "unexpected trailing input"
sumElements : Bool -> Int ->{Pull.Pull} Int
sumElements first total =
if Pull.nextElement first then
number = Pull.decodeValue()
sumElements False (total + number)
else total
main() =
print (Reader.withBytes (Bytes.fromString "[1, 2, 3]") { reader ->
Pull.withReader reader sumArray
})
Ok 6
Pass True for the first element and False after that. nextElement consumes the closing bracket when the array ends. The final EndInput check makes sure the action consumes a whole document, instead of accepting a valid array followed by extra content.
Failure is part of Pull itself, so a decoder’s row is just {Pull.Pull}, with no separate Fail. A failure, whether from Pull.fail or from malformed input, ends the whole document, and withReader returns it as Err.
For objects, use beginObject and nextKey. skipValue validates a value and throws it away, and readValue() builds a generic tree for just the part you need. Wrap nested work in withKey or withIndex so that errors name the field or element involved.
Custom decoders.
A handwritten Decode instance implements scanValue. If you’re wrapping a representation that’s already supported, you can transform an existing decoder:
import Json
import Json.Pull as Pull
type Port = Port Int deriving (Show)
instance Pull.Decode Port
scanValue cursor = Pull.mapScan Port (Pull.scanValue cursor)
main() = print (Json.parse @Port "8080")
Ok (Port 8080)
The constructor Port takes an Int, so the inner scanValue picks the bundled integer decoder. mapScan wraps its successful value and keeps its buffered or suspended progress intact.
When decoding needs validation or several token operations, use scanFromPull cursor { ... }. The callback runs once the common driver is ready, decodeValue() can hand off to child decoders, and fail can reject an invalid value. Import Decode(..) if you want the class and its method unqualified.
Test with successful input, with tokens of the wrong type, and with input split across chunks. A decoder has to work when the reader refills halfway through a value, not only when the whole document is already buffered.
Buffered scans.
The scan protocol keeps inspecting buffered text separate from work that may need more input. scanValue starts with an opaque speculative cursor and returns one of two states:
| Result | Meaning |
|---|---|
Ok (value, rest) |
The value is complete in the buffer; rest is the next cursor. |
Err action |
The driver must run suspended work, possibly refilling input. |
Here Err is a way of representing control flow, not a JSON error. Constructing its action consumes and refills nothing. The driver runs the action, resumes scanning with bounded stack use, and publishes the final position.
Derived decoders keep completed fields and scalar progress across refills, and handwritten decoders should do the same. Redoing work that already succeeded can repeat effects or lose track of what’s been consumed. For most custom wrappers, mapScan or scanFromPull handles this bookkeeping for you, and ordinary Json.read callers never need to look at scan results at all.
