jsonschema

JSON schema validation and creation.


Keywords
json, schema, library, validation
License
MIT
Install
nimble install jsonschema

Documentation

JSON schema validation

When working with JSON data in Nim you must ensure that all the keys you require are present and that the values they store are of the correct type. Failure to do so will result in exceptions and make your program fragile. The jsonschema module implements a schema DSL that generates verifiers and creators for structured JSON data. The DSL is based around TypeScript, but is not 100% compatible (yet). In the test file tests/test1.nim there are some example of how to use the module. What follows is a commented explanation of how the DSL works.

jsonSchema: # The macro that parses the schema DSL
  # `CancelParams` here is the name we want to give this schema, it doesn't
  # verify against anything and is just use to refer to it in your code.
  CancelParams:
    # Optional fields are denoted with "?:" they aren't required to show up
    # in the JSON, but will get verified if they do. When you want to
    # create an object matching this schema you need to pass an Option[T]
    # type for it. This also shows how one key can have multiple different
    # allowed types. When you use the value you must check which of the
    # types is actually present.
    id?: int or string or float
    # Same as above, checks if a key is present, and verifies it's type if
    # it does. Note that with optional fields you have to check if it
    # actually exists in the JSON object before using them.
    something?: float

  WrapsCancelParams:
    # Referring to another schema declared in the same `jsonSchema` block
    # means that this is an object within this schema. The validator proc
    # has a `traverse` option, which defaults to true, that dictates if the
    # verification shall check nested schemas or not.
    cp: CancelParams
    # Neither this nor the `cp` field is optional, they have to exist in
    # the JSON object or it won't be valid.
    name: string

  # This style of declaration means that all the fields in `CancelParams`
  # will also be verified for `ExtendsCancelParams`. Any extra fields will
  # also be verified. In the generated `create` procedure the order is
  # extended arguments first, then the new arguments.
  ExtendsCancelParams extends CancelParams:
    name: string

  WithArrayAndAny:
    # This is an array declaration. Arrays are homongenous and all elements
    # will be verified against the type. In this case they will not be
    # verified if `traverse` is set to false, althought the array will be
    # checked to actually be an array.
    test?: CancelParams[]
    # This is exactly what you would expect, either an array of integers,
    # or a single float.
    ralph: int[] or float
    # The keyword `any` can be used for any JSON value, including arrays
    # and objects. In this case it will be verified that the field exists,
    # but it won't get checked in any way outside that.
    bob: any
    # JSON also has a `null` value, this can be specified with the `nil`
    # keyword. This is currently implemented as an enum `NilType` with a
    # single value `Nil`. So for an optional field you can specify its
    # absence with `none(NilType)` and with a value as `some(Nil)`
    john?: int or nil

  NameTest:
    # In cases where the name of the field would collide with a Nim keyword
    # you can quote it. The field is still checked for the same name, but
    # the argument name is mangled. By default a prefix of "the" is added,
    # but should this collide with something you can change the prefix with
    # `-d:ManglePrefix="<your mangle prefix>"
    "method": string
    "result": int
    "if": bool
    "type": float

As an example of the code generated this is what the above WithArrayAndAny would generate. Note that you can also get the code generated with -d:jsonSchemaDebug.

type
  WithArrayAndAny = distinct JsonNode

proc isValid(data: JsonNode; schemaType: typedesc[WithArrayAndAny];
            traverse = true): bool =
  if data.kind != JObject:
    return false
  var fields = 2
  if data.hasKey("test"):
    fields += 1
    if data["test"].kind != JArray or
        (traverse and
        not data["test"].allIt(it.isValid(CancelParams))):
      return false
  if not data.hasKey("ralph"):
    return false
  if data["ralph"].kind != JArray or
      data["ralph"].anyIt(it.kind != Jint) and
      data["ralph"].kind != Jfloat:
    return false
  if not data.hasKey("bob"):
    return false
  if false:
    return false
  if data.hasKey("john"):
    fields += 1
    if data["john"].kind != Jint and
        data["john"].kind != JNull:
      return false
  if fields !=
      data.len:
    return false
  return true

proc create(schemaType: typedesc[WithArrayAndAny]; test: Option[seq[CancelParams]];
           ralph: seq[int] or float; bob: JsonNode;
           john: Option[int] or Option[NilType]): WithArrayAndAny =
  var ret = newJObject()
  when test is
      Option[seq[CancelParams]]:
    if test.isSome:
      []=(ret, "test", newJArray())
      for i in test.get:
        ret["test"].add i.JsonNode
  when ralph is seq[int]:
    []=(ret, "ralph", newJArray())
    for i in ralph:
      ret["ralph"].add %i
  when ralph is float:
    []=(ret, "ralph", %ralph)
  when bob is JsonNode:
    []=(ret, "bob", bob.JsonNode)
  when john is
      Option[int]:
    if john.isSome:
      []=(ret, "john", %john.get)
  when john is
      Option[NilType]:
    if john.isSome:
      []=(ret, "john", newJNull())
  return ret.WithArrayAndAny