LoginSignup
2
1

More than 1 year has passed since last update.

JSON SchemaからNimのオブジェクトを自動生成する

Last updated at Posted at 2022-12-03

この記事はNim Advent Calendar 2022の4日目の記事です。

JSON SchemaからNimのオブジェクト定義を自動生成する話です。

JSON Schemaとは

オブジェクト定義をJSONで表現する規格です。
主な用途としてはWebAPIでフロントとサーバ間でやりとりするJSONを定義するケースです。

例えば、以下のようなJSON Schemaで表現したJSONがあります。

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://nats.io/schemas/jetstream/advisory/v1/nak.json",
  "description": "Advisory published when a message was naked using a AckNak acknowledgement",
  "title": "io.nats.jetstream.advisory.v1.nak",
  "type": "object",
  "required": [
    "type",
    "id",
    "timestamp",
    "stream",
    "consumer",
    "consumer_seq",
    "stream_seq",
    "deliveries"
  ],
  "additionalProperties": false,
  "properties": {
    "type": {
      "type": "string",
      "const": "io.nats.jetstream.advisory.v1.nak"
    },
    "id": {
      "type": "string",
      "description": "Unique correlation ID for this event"
    },
    "timestamp": {
      "type": "string",
      "description": "The time this event was created in RFC3339 format"
    },
    "stream": {
      "type": "string",
      "description": "The name of the stream where the message is stored"
    },
    "consumer": {
      "type": "string",
      "description": "The name of the consumer where the message was naked"
    },
    "consumer_seq": {
      "type": "string",
      "minimum": 1,
      "description": "The sequence of the message in the consumer that was naked"
    },
    "stream_seq": {
      "type": "string",
      "minimum": 1,
      "description": "The sequence of the message in the stream that was naked"
    },
    "deliveries": {
      "type": "integer",
      "minimum": 1,
      "description": "The number of deliveries that were attempted"
    },
    "domain": {
      "type": "string",
      "minimum": 1,
      "description": "The domain of the JetStreamServer"
    }
  }
}

このJSON Schemaは、以下のNimオブジェクトを表現したものです。
前述のJSON Schemaには minimum などの、値の下限と上限も記述できるため、
パラメータの境界値も明確になります。

type
  Object = ref object
    `type`: string
    id: string
    timestamp: string
    stream: string
    consumer: string
    consumer_seq: string
    stream_seq: string
    deliveries: int64
    domain: Option[string]

JSON Schemaについての話はすでにQiitaにいくつか解説している記事が存在するので、そちらに譲ります。
以下の記事は少し古いですが、最初の導入としてとても分かりやすかったです。

公式のドキュメントは以下です。

JSON Schemaと似たようなものとしてはOpenAPI Spedificationが該当します。
こちらも便利です。

JSON SchemaからNimのオブジェクト定義を生成する

本題です。JSON SchemaからNimのオブジェクト定義を生成します。
手前味噌ですが、nimjsonというツールを使うことで、JSON SchemaからNimのオブジェクト定義を生成できます。

以下のように使います。簡単ですね。

$ nimjson -j examples/json_schema.json
type
  Object = ref object
    `type`: string
    id: string
    timestamp: string
    stream: string
    consumer: string
    consumer_seq: string
    stream_seq: string
    deliveries: int64
    domain: Option[string]

経緯

nimjsonは3年ほど昔に僕が作成したNim製のCLIツールです。
以下の記事で解説しています。

もともとnimjsonは、すでに存在するJSON文字列から
Nimのオブジェクト定義を逆生成する用途で作成したツールでした。
もとはgojsonというツールを参考にしたツールです。

$ echo '{"name":"hello"}' | nimjson
type
  NilType = ref object
  Object = ref object
    name: string

よってJSON Schemaのことは全くサポートしていませんでした。
僕自身、このMRで要望をもらって初めてJSON Schemaを知りました。

Nimのオブジェクト定義を生成するのが目的のツールなので、
用途的にもおかしくないので、せっかくだしこの要望に応えて、機能追加することにしました。

実装

実装の詳細はリポジトリのソースコードを見れば良いので、ここにすべては書きません。
メインのロジックだけ話すと、以下のparseプロシージャが肝です。

proc parse(parser: var JsonSchemaParser, property: Property,
    objectName: string) =
  if not property.isTypeObject:
    let typ =
      if property.isTypeArray: property.items.`type`
      else: property.`type`
    let objDef = newObjectDefinition(objectName.headUpper, false,
        parser.isPublic, parser.forceBackquote, typ, property.isTypeArray)
    parser.defs.add(objDef)
    return

  var objDef = newObjectDefinition(objectName.headUpper, false, parser.isPublic,
      parser.forceBackquote)
  for propName, prop in property.properties:
    let isOption = (not parser.disableOptionType) and propName notin
        property.required
    let typ =
      if prop.hasRef: prop.getRefTypeName(propName)
      else: prop.getPropertyType(propName)
    let fDef = newFieldDefinition(propName, typ, parser.isPublic,
        parser.forceBackquote, prop.isTypeArray, isOption)
    objDef.addFieldDefinition(fDef)
    if prop.isTypeObject:
      let p = newProperty(
        prop.description,
        prop.`type`,
        prop.required,
        prop.properties,
        prop.`$ref`,
      )
      parser.parse(p, typ)
  parser.defs.add(objDef)

proc parseAndGetString*(s: string, objectName: string, isPublic: bool,
    forceBackquote: bool, disableOptionType: bool): string =
  var parser = JsonSchemaParser(
    isPublic: isPublic,
    forceBackquote: forceBackquote,
    disableOptionType: disableOptionType,
  )

  let schema = s.fromJson(JsonSchema)
  let property = newProperty(
    schema.description,
    schema.`type`,
    schema.required,
    schema.properties,
    "",
  )
  parser.parse(property, objectName)

  for propName, prop in schema.`$defs`:
    parser.parse(prop, propName)

  result.add("type\n")
  result.add(parser.defs.toDefinitionString())

JSON SchemaのJSON文字列を解析して、
nimjson固有のオブジェクト定義オブジェクト(ObjectDefinition)に変換しています。

オブジェクト定義オブジェクトは、過去にnimjsonを作ったときから存在する型で
Nimのオブジェクト定義文字列に変換するプロシージャを持っています。
既存の文字列変換を使い回せるので、オブジェクト定義オブジェクトへ変換しています。

この既存の文字列生成プロシージャを使いまわしたかったため
オブジェクト定義オブジェクトに変換しています。

JSON Schemaではオブジェクトのネストが起こり得るため、parseを再帰呼び出しして処理しています。

また、JSON Schema自体をNimのオブジェクトへバインドするのにjsonyというライブラリを使っています。
fromJson というプロシージャ呼び出しがそれです。

Nim標準の、JSONをObjectにバインドするtoプロシージャには問題があります。
JSONには存在しないキーがバインド先オブジェクトに存在する場合エラーが発生します。
JSON Schemaで言うと、requiredキーワードなどは、必ずしも必要ではないため、実行時エラーになる可能性がありました。
jsonyはこの問題を解決してくれるため、採用しました。

jsonyを採用したことでNimコンパイラの1.0系、1.2系のサポートを打ち切らざるを得なかったのが残念です。
まぁ、開発時のツールとしての用途がメインなので、古いバージョンでインストールする人はあんまりいないはずですがね。

まとめ

以下の話をしました。

  1. JSON Schemaの話を軽くしました
  2. JSON SchemaからNimのオブジェクト定義を生成するツールnimjsonの使い方を話しました
  3. nimjsonの実装について話しました

以上。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1