Edited at

JSONからNimのObject定義を生成するコマンドnimjsonを作った


はじめに

NimでJSONを読み込んでObjectに変換するとき、以下のように実装できます。


point.json

{"x":10, "y":20}


import json

type
Point = ref object
x: int
y: int

let point = parseFile("point.json").to(Point)
echo point[]

toでオブジェクトマッピングするにはObject定義が必要です。

このケースは簡単ですが、JSONが複雑な構造をしていたり、非常に大量のキーを持っていたりした際に

それを手動で定義するのは非常に大変な作業です。

今回作成したnimjsonはこの問題を少しでも簡単にするためのコマンドです。

gojsonというJSONからGoの構造体定義を生成するツールに影響を受けました。


ソースコード


インストール

Nimのバージョンは下記の通り。

% nim -v

Nim Compiler Version 0.20.0 [Linux: amd64]
Compiled at 2019-06-06
Copyright (c) 2006-2019 by Andreas Rumpf

git hash: e7471cebae2a404f3e4239f199f5a0c422484aac
active boot switches: -d:release

% nimble -v
nimble v0.10.2 compiled at 2019-06-15 22:10:02
git hash: couldn't determine git hash

インストールコマンド。

nimble install nimjson

コマンドだけ必要な場合はReleaseからバイナリをダウンロードしてください。


使い方 (コマンド)

非常に簡単なJSONからNimのObject定義を生成してみます。

% echo '{"x":10.0, "y":20.0, "width":144, "height":144}' | nimjson

type
NilType = ref object
Object = ref object
x: float64
y: float64
width: int64
height: int64

nimjsonのリポジトリの情報からNimのObject定義を生成してみます。

Nimではネストするオブジェクトの定義ができないので、JSONがネストするオブジェクト型の値を持っていた場合は

ネストしないようにわけて定義しています。

% curl -s https://api.github.com/repos/jiro4989/nimjson | nimjson -O:Repository

type
NilType = ref object
Repository = ref object
id: int64
node_id: string
name: string
full_name: string
private: bool
owner: Owner
html_url: string
description: string
fork: bool
url: string
forks_url: string
keys_url: string
collaborators_url: string
teams_url: string
hooks_url: string
issue_events_url: string
events_url: string
assignees_url: string
branches_url: string
tags_url: string
blobs_url: string
git_tags_url: string
git_refs_url: string
trees_url: string
statuses_url: string
languages_url: string
stargazers_url: string
contributors_url: string
subscribers_url: string
subscription_url: string
commits_url: string
git_commits_url: string
comments_url: string
issue_comment_url: string
contents_url: string
compare_url: string
merges_url: string
archive_url: string
downloads_url: string
issues_url: string
pulls_url: string
milestones_url: string
notifications_url: string
labels_url: string
releases_url: string
deployments_url: string
created_at: string
updated_at: string
pushed_at: string
git_url: string
ssh_url: string
clone_url: string
svn_url: string
homepage: string
size: int64
stargazers_count: int64
watchers_count: int64
language: string
has_issues: bool
has_projects: bool
has_downloads: bool
has_wiki: bool
has_pages: bool
forks_count: int64
mirror_url: NilType
archived: bool
disabled: bool
open_issues_count: int64
license: License
forks: int64
open_issues: int64
watchers: int64
default_branch: string
network_count: int64
subscribers_count: int64
Owner = ref object
login: string
id: int64
node_id: string
avatar_url: string
gravatar_id: string
url: string
html_url: string
followers_url: string
following_url: string
gists_url: string
starred_url: string
subscriptions_url: string
organizations_url: string
repos_url: string
events_url: string
received_events_url: string
type: string
site_admin: bool
License = ref object
key: string
name: string
spdx_id: string
url: string
node_id: string

型がnullのときはNilTypeが定義されます。

NilTypeがセットされるのは以下の2ケースです。


  1. 値がnullである

  2. 配列の最初の要素がnullである

NilTypeが嫌な場合は手動で修正する必要があります。

あるいは元のJSONにnullでない値をセットして再度nimjsonを実行する必要があります。


使い方 (Web)

JSにコンパイルしてGitHub Pagesで使えるようにしたWeb版もあります。

インストールせずに使いたい場合はこれで十分かと。


実装の参考

gojsonから参考にしたのは達成したいことに対するコマンドのIn/Outのインタフェースを参考にしました。

内部実装で参考にしたのはNimの標準ライブラリjsontoUgryというプロシージャです。

proc toUgly*(result: var string, node: JsonNode) =

## Converts `node` to its JSON Representation, without
## regard for human readability. Meant to improve ``$`` string
## conversion performance.
##
## JSON representation is stored in the passed `result`
##
## This provides higher efficiency than the ``pretty`` procedure as it
## does **not** attempt to format the resulting JSON to make it human readable.
var comma = false
case node.kind:
of JArray:
result.add "["
for child in node.elems:
if comma: result.add ","
else: comma = true
result.toUgly child
result.add "]"
of JObject:
result.add "{"
for key, value in pairs(node.fields):
if comma: result.add ","
else: comma = true
key.escapeJson(result)
result.add ":"
result.toUgly value
result.add "}"
of JString:
node.str.escapeJson(result)
of JInt:
when defined(js): result.add($node.num)
else: result.add(node.num)
of JFloat:
when defined(js): result.add($node.fnum)
else: result.add(node.fnum)
of JBool:
result.add(if node.bval: "true" else: "false")
of JNull:
result.add "null"

NimでJSON文字列をパースして得られるオブジェクトのJsonNodeは(個人的には)特殊な型で、

kindフィールドの型によってアクセス可能なフィールドが決まるという型です。

以下のように、kindの型によって分岐するような実装になっています。

こういう実装の仕方があるんだなぁと。

type

JsonNode* = ref JsonNodeObj ## JSON node
JsonNodeObj* {.acyclic.} = object
case kind*: JsonNodeKind
of JString:
str*: string
of JInt:
num*: BiggestInt
of JFloat:
fnum*: float
of JBool:
bval*: bool
of JNull:
nil
of JObject:
fields*: OrderedTable[string, JsonNode]
of JArray:
elems*: seq[JsonNode]


最後に

NimでJSONを扱う際の助けになれば幸いです。