Getting started
- Imports and exports
- Simple reader
- Encoding and pretty printing
- Handling errors
- Custom parsing
- Flavors and strictness
- Required and optional fields
- Automatic object conversion
- ...almost there!
json_serialization
is used to parse JSON documents directly into Nim types and to encode them back as JSON efficiently.
Let's start with a simple JSON-RPC example:
{"jsonrpc": "2.0", "method": "subtract", "params": [42, 3], "id": 1}
Imports and exports
Before we can use json_serialization
, we have to import the library.
If you put your custom serialization code in a separate module, make sure to re-export json_serialization
:
{.push gcsafe, raises: [].} # Encourage exception handling hygiene in procedures!
import json_serialization
export json_serialization
A common way to organize serialization code is to use a separate module named either after the library (mylibrary_json_serialization
) or the flavor (myflavor_json_serialization
).
For types that mainly exist to interface with JSON, custom serializers can also be placed together with the type definitions.
When importing a module that contains custom serializers, make sure to re-export it or you might end up with cryptic compiler errors or worse, the default serializers being used!
Simple reader
Looking at the example, we'll define a Nim object
to hold the request data, with matching field names and types:
type Request = object
jsonrpc: string
`method`: string # Quote Nim keywords
params: seq[int] # Map JSON array to `seq`
id: int
Json.decode
can now turn our JSON input into a Request
:
# Decode the string into our Request type
let decoded = Json.decode(
"""{"jsonrpc": "2.0", "method": "subtract", "params": [42, 3], "id": 1}""", Request
)
echo decoded.id
Replace decode
/encode
with loadFile
/saveFile
to read and write a file instead!
Encoding and pretty printing
Having parsed the example with Json.decode
, we can pretty-print it back to the console using Json.encode
that returns a string
:
# Now that we have a `Request` instance, we can pretty-print it:
echo Json.encode(decoded, pretty = true)
Handling errors
Of course, someone might give us some invalid data - json_serialization
will raise an exception when that happens:
try:
# Oops, a string was used for the `id` field!
discard Json.decode("""{"id": "test"}""", Request)
except JsonError as exc:
# "<string>" helps identify the source of the document - this can be a
# filename, URL or something else that helps the user find the error
echo "Failed to parse document: ", exc.formatMsg("<string>")
The error message points out where things went wrong:
Failed to parse document: <string>(1, 8) number expected
Custom parsing
Happy we averted a crisis by adding the forgotten exception handler, we go back to the JSON-RPC specification and notice that strings are actually allowed in the id
field - further, the only thing we have to do with id
is to pass it back in the response - we don't really care about its contents.
We'll define a helper type to deal with this situation and attach some custom parsing code to it that checks the type. Using JsonString
as underlying storage is an easy way to pass around snippets of JSON whose contents we don't need.
The custom code is added to readValue
/writeValue
procedures that take the stream and our custom type as arguments:
type JsonRpcId = distinct JsonString
proc readValue*(
r: var JsonReader, val: var JsonRpcId
) {.raises: [IOError, JsonReaderError].} =
let tok = r.tokKind
case tok
of JsonValueKind.Number, JsonValueKind.String, JsonValueKind.Null:
# Keep the original value without further processing
val = JsonRpcId(r.parseAsString())
else:
r.raiseUnexpectedValue("Invalid RequestId, got " & $tok)
proc writeValue*(w: var JsonWriter, val: JsonRpcId) {.raises: [IOError].} =
w.writeValue(JsonString(val)) # Preserve the original content
Flavors and strictness
While the defaults that json_serialization
offers are sufficient to get started, implementing JSON-based standards often requires more fine-grained control, such as what to do when a field is missing, unknown or has high-level requirements for parsing and formatting.
We use createJsonFlavor
to declare the new flavor passing to it the customization options that we're interested in:
createJsonFlavor JrpcSys,
automaticObjectSerialization = false,
requireAllFields = true,
omitOptionalFields = true, # Don't output `none` values when writing
allowUnknownFields = false
Required and optional fields
In the JSON-RPC example, both the jsonrpc
version tag and method
are required while parameters and id
can be omitted. Our flavor required all fields to be present except those explicitly optional - we use Opt
from results to select the optional ones:
type Request = object
jsonrpc: string
`method`: string
params: Opt[seq[int]]
id: Opt[JsonRpcId]
Automatic object conversion
The default Json
flavor allows any object
to be converted to JSON. If you define a custom serializer and someone forgets to import it, the compiler might end up using the default instead resulting in a nasty runtime surprise.
automaticObjectSerialization = false
forces a compiler error for any type that has not opted in to be serialized:
# Allow serializing the `Request` type - serializing other types will result in
# a compile-time error because `automaticObjectSerialization` is false!
JrpcSys.useDefaultSerializationFor Request
With all that work done, we can finally use our custom flavor to encode and decode the Request
:
const json = """{"jsonrpc": "2.0", "method": "subtract", "params": [42, 3], "id": 1}"""
echo JrpcSys.encode(JrpcSys.decode(json, Request))
...almost there!
While we've covered a fair bit of ground already, our Request
parser is still not fully standards-compliant - in particular, the list of parameters must be able to handle both positional and named arguments and the values can themselves be full JSON documents that need custom parsing based on the method
value.
A more mature JSON-RPC parser can be found in nim-json-rpc which connects the json_serialization
library to a DSL that conveniently allows mapping Nim procedures to JSON-RPC methods, featuring automatic parameter conversion and other nice conveniences..
Furtyher examples of how to use json_serialization
can be found in the tests
folder.