Fork me on GitHub
NimYAML Home Testing Ground Docs: Overview Serialization Modules

Serialization Overview

Introduction

NimYAML tries hard to make transforming YAML characters streams to native Nim types and vice versa as easy as possible. In simple scenarios, you might not need anything else than the two procs dump and load. On the other side, the process should be as customizable as possible to allow the user to tightly control how the generated YAML character stream will look and how a YAML character stream is interpreted.

An important thing to remember in NimYAML is that unlike in interpreted languages like Ruby, Nim cannot load a YAML character stream without knowing the resulting type beforehand. For example, if you want to load this piece of YAML:

%YAML 1.2
--- !nim:system:seq(nim:system:int8)
- 1
- 2
- 3

You would need to know that it will load a seq[int8] at compile time. This is not really a problem because without knowing which type you will load, you cannot do anything useful with the result afterwards in the code. But it may be unfamiliar for programmers who are used to the YAML libraries of Python or Ruby.

Supported Types

NimYAML supports a growing number of types of Nim's system module and standard library, and it also supports user-defined object, tuple and enum types out of the box. A complete list of explicitly supported types is available in Schema.

Important: NimYAML currently does not support polymorphism. This may be added in the future.

This also means that NimYAML is generally able to work with object, tuple and enum types defined in the standard library or a third-party library without further configuration, given that all fields of the object are accessible at the code point where NimYAML's facilities are invoked.

Scalar Types

The following integer types are supported by NimYAML: int, int8, int16, int32, int64, uint8, uint16, uint32, uint64. Note that the int type has a variable size dependent on the target operation system. To make sure that it round-trips properly between 32-bit and 64-bit operating systems, it will be converted to an int32 during loading and dumping. This will raise an exception for values outside of the range int32.low .. int32.high! If you define the types you serialize yourself, always consider using an integer type with explicit length. The same goes for uint.

The floating point types float, float32 and float64 are also supported. There is currently no problem with float, because it is always a float64.

string is supported and one of the few Nim types which directly map to a standard YAML type. NimYAML is able to handle strings that are nil, they will be serialized with the special tag !nim:nil:string. char is also supported.

To support new scalar types, you must implement the constructObject() and representObject() procs on that type (see below).

Container Types

NimYAML supports Nim's array, set, seq, Table, OrderedTable and Option types out of the box. While YAML's standard types !!seq and !!map allow arbitrarily typed content, in Nim the contained type must be known at compile time. Therefore, Nim cannot load !!seq and !!map.

However, it doesn't need to. For example, if you have a YAML file like this:

%YAML 1.2
---
- 1
- 2

You can simply load it into a seq[int]. If your YAML file contains differently typed values in the same collection, you can use an implicit variant object, see below.

A special case is Option[T]: This type will either contain a value or not. NimYAML maps !!null YAML scalars to the option's none(T) value. This also works for ref types because Option for those types will use nil as its none(T) value.

By default, Option fields must be given even if they are none(T). You can circumvent this by putting the annotation {.sparse.} on the type containing the Option field.

Reference Types

A reference to any supported non-reference type (including user defined types, see below) is supported by NimYAML. A reference type will be treated like its base type, but NimYAML is able to detect multiple references to the same object and dump the structure properly with anchors and aliases in place. It is possible to dump and load cyclic data structures without further configuration. It is possible for reference types to hold a nil value, which will be mapped to the !!null YAML scalar type.

ptr types are not supported because it seems dangerous to automatically allocate memory which the user must then manually deallocate.

User Defined Types

For an object or tuple type to be directly usable with NimYAML, the following conditions must be met:

NimYAML will present enum types as YAML scalars, and tuple and object types as YAML maps. Some of the conditions above may be loosened in future releases.

Variant Object Types

A variant object type is an object type that contains one or more case clauses. NimYAML supports variant object types. Only the currently accessible fields of a variant object type are dumped, and only those may be present when loading.

The value of a discriminator field must be loaded before any value of a field that depends on it. Therefore, a YAML mapping cannot be used to serialize variant object types - the YAML specification explicitly states that the order of key-value pairs in a mapping must not be used to convey content information. So, any variant object type is serialized as a list of key-value pairs.

For example, this type:

type
  AnimalKind = enum
    akCat, akDog
  
  Animal = object
    name: string
    case kind: AnimalKind
    of akCat:
      purringIntensity: int
    of akDog:
      barkometer: int

will be serialized as:

%YAML 1.2
--- !nim:custom:Animal
- name: Bastet
- kind: akCat
- purringIntensity: 7

You can also use variant object types for processing heterogeneous data sets. For example, if you have a YAML document which contains differently typed values in the same list like this:

%YAML 1.2
---
- 42
- this is a string
- !!null

You can define a variant object type that can hold all types that occur in this list in order to load it:

import yaml

type
  ContainerKind = enum
    ckInt, ckString, ckNone
  Container {.implicit.} = object
    case kind: ContainerKind
    of ckInt:
      intVal: int
    of ckString:
      strVal: string
    of ckNone:
      discard

var
  list: seq[Container]
  s = newFileStream("in.yaml")
load(s, list)

{.implicit.} tells NimYAML that you want to use the type Container implicitly, i.e. its fields are not visible in YAML, and are set dependent on the value type that gets loaded into it. The type Container must fullfil the following requirements:

When loading the sequence, NimYAML writes the value into the first field that can hold the value's type. All complex values (i.e. non-scalar values) must have a tag in the YAML source, because NimYAML would otherwise be unable to determine their type. The type of scalar values will be guessed if no tag is available, but be aware that 42 can fit in both int8 and int16, so in the case you have fields for both types, you should annotate the value.

When dumping the sequence, NimYAML will always annotate a tag to each value it outputs. This is to avoid possible ambiguity when loading. If a branch without a field exists, it is represented as a !!null value.

Tags

NimYAML uses local tags to represent Nim types that do not map directly to a YAML type. For example, int8 is presented with the tag !nim:system:int8. Tags are mostly unnecessary when loading YAML data because the user already defines the target Nim type which usually defines all types of the structure. However, there is one case where a tag is necessary: A reference type with the value nil is represented in YAML as a !!null scalar. This will be automatically detected by type guessing, but if it is for example a reference to a string with the value "~", it must be tagged with !!string, because otherwise, it would be loaded as nil.

As you might have noticed in the example above, the YAML tag of a seq depends on its generic type parameter. The same applies to Table. So, a table that maps int8 to string sequences would be presented with the tag !n!tables:Table(tag:nimyaml.org,2016:int8,tag:nimyaml.org,2016:system:seq(tag:yaml.org,2002:string)). These tags are generated on the fly based on the types you instantiate Table or seq with.

You may customize the tags used for your types by using the template setTagUri. It may not be applied to scalar and collection types implemented by NimYAML, but you can for example use it on a certain seq type:

setTagUri(seq[string], "!nim:my:seq")

Customizing Field Handling

NimYAML allows the user to specify special handling of certain object fields via annotation pragmas.

Transient Fields

It may happen that certain fields of an object type are transient, i.e. they are used in a way that makes (de)serializing them unnecessary. Such fields can be marked as transient. This will cause them not to be serialized to YAML. They will also not be accepted when loading the object.

Example:

type MyObject: object
  storable: string
  temporary {.transient.}: string

Default Values

When you load YAML, you might want to allow for the omission certain fields, which should then be filled with a default value. You can do that like this:

type MyObject: object
  required: string
  optional {.defaultVal: "default value".}: string

Whenever a value of type MyObject now is loaded and the input stream does not contain the field optional, that field will be set to the value "default value".

Customize Serialization

It is possible to customize the serialization of a type. For this, you need to implement two procs, constructObject̀ and representObject. If you only need to process the type in one direction (loading or dumping), you can omit the other proc.

constructObject

proc constructObject*(s: var YamlStream, c: ConstructionContext,
                      result: var MyObject)
        {.raises: [YamlConstructionError, YamlStreamError.}

This proc should construct the type from a YamlStream. Follow the following guidelines when implementing a custom constructObject proc:

The following example for constructing from a YAML scalar value is the actual implementation of constructing int types:

proc constructObject*[T: int8|int16|int32|int64](
        s: var YamlStream, c: ConstructionContext, result: var T)
        {.raises: [YamlConstructionError, YamlStreamError].} =
    var item: YamlStreamEvent
    constructScalarItem(s, item, name(T)):
        result = T(parseBiggestInt(item.scalarContent))

The following example for constructiong from a YAML non-scalar is the actual implementation of constructing seq types:

proc constructObject*[T](s: var YamlStream, c: ConstructionContext,
                         result: var seq[T])
        {.raises: [YamlConstructionError, YamlStreamError].} =
    let event = s.next()
    if event.kind != yamlStartSequence:
        raise newException(YamlConstructionError, "Expected sequence start")
    result = newSeq[T]()
    while s.peek().kind != yamlEndSequence:
        var item: T
        constructChild(s, c, item)
        result.add(item)
    discard s.next()

representObject

proc representObject*(value: MyObject, ts: TagStyle = tsNone,
                      c: SerializationContext, tag: TagId): {.raises: [].}

This proc should push a list of tokens that represent the type into the serialization context via c.put. Follow the following guidelines when implementing a custom representObject proc:

The following example for representing to a YAML scalar is the actual implementation of representing int types:

proc representObject*[T: int8|int16|int32|int64](value: T, ts: TagStyle,
    c: SerializationContext, tag: TagId) {.raises: [].} =
  ## represents an integer value as YAML scalar
  c.put(scalarEvent($value, tag, yAnchorNone))

The following example for representing to a YAML non-scalar is the actual implementation of representing seq and set types:

proc representObject*[T](value: seq[T]|set[T], ts: TagStyle,
    c: SerializationContext, tag: TagId) {.raises: [YamlStreamError].} =
  ## represents a Nim seq as YAML sequence
  let childTagStyle = if ts == tsRootOnly: tsNone else: ts
  c.put(startSeqEvent(tag))
  for item in value:
    representChild(item, childTagStyle, c)
  c.put(endSeqEvent())