Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Getting started

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.

Re-exports

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.

Read that spec!

Not only did we learn to about json_serialization, but also that examples are no substitute for reading the spec!