A JSON toolkit for MoonBit: format, validate, diagnose, analyse and visualise JSON documents.
Dependencies

error: <stdin> is not valid JSON
line 1, column 8: expected opening quote
{"a":1,}
^A line too long to print is elided around the column, the two ends it cut
replaced by ..., so a document written on one line is not reported at its
full length — the line and column in the summary are always the ones in the
document, and the caret is the recomputed one.A repeated key inside one object is an error for the same reason, reported at
the second spelling of the key and naming the line and column of the first —
silently keeping one of the two would mean the value you get back depends on
the parser rather than on the document. --max-depth <n> puts the same kind of
bound on nesting: any document deeper than n is refused, with the position
where it went too far.git clone https://github.com/BigSaltyMan/moonjson-toolkit.git
cd moonjson-toolkit
moon build --target nativemoon add Nanaloveyuki/parsec # parser combinators, with a JSON grammar
moon add oboard/mio # HTTP client used by --aimoonjson-toolkit - format, validate and analyse JSON documents
Usage:
moonjson-toolkit [options]
Input is read from standard input unless --file is given.
Options:
-f, --file <path> Read one or more input files instead of standard input
-i, --indent <n> Spaces per nesting level, 0 to 16 (default: 2)
-c, --compact Print the document on one line, ignoring --indent
-S, --sort-keys Order the keys of every object before printing
--trim-strings Trim the whitespace around every string in the document
--flatten Collapse every nested object into dotted keys
--unflatten Expand every dotted key back into nested objects
--prune-null Remove every object member whose value is null
--prune-empty Remove every empty object and every empty array
--select <fields> Keep only the named top-level fields of an object
--sort-by <path> Sort an array by the value <path> names in each item
--unique [path] Drop repeated items, whole ones or by the value at <path>
--jsonl Read the input as JSON Lines, one document per line
--max-depth <n> Refuse documents nested deeper than <n> (default: 128)
--paths Print the path of every value instead of the document
--keys-only Print only the paths that name an object member
-v, --validate Only check the input and report the first error
--schema <path> Check the input against the JSON Schema in <path>
-h, --help Show this message and exit
-V, --version Show the version and exit
--ai Ask the AI for a quality report and suggestions
--model <name> Model to ask for instead of the default
--ai-base-url <url>
Endpoint to post to instead of the default
--moon-deps Print the dependency tree of a MoonBit module
manifest
--fail-fast Stop at the first input that fails
--continue-on-error
Process every input, then summarise the failures
--json-out <path> Write a statistics report as JSON to <path>
--stats Print a statistics summary instead of the document
--emit-moonbit [name]
Print the MoonBit type of the document's shape
--no-color Never colour the output, even on a terminal
Short options may be combined, so -vh means -v -h. The value of --indent
may be attached, as in -i4, -i=4, --indent=4 or --indent 4.
--file may be repeated. Each file is handled in turn, each under a
'==> path <==' heading once more than one is given, and the run exits with
the first non-zero code among them.
--fail-fast stops the run at the first input that fails, so a batch of
files ends at the one that went wrong rather than at the end of the list.
Without it every input named is read and every failure is reported as it
happens, which is the default; --continue-on-error asks for that same
reading and adds a summary of it at the end, naming each input that failed
and the message it failed with. The two describe one run in opposite
directions, so asking for both is refused. Under --jsonl the input is one
file however many records it holds, so --fail-fast stops at the first
record that fails, and --continue-on-error summarises the file.
--paths and --keys-only each replace the printed document with a list of
names, one per line. Asking for both prints the longer list, and -v wins
over either of them, since it asks for no document output at all.
--stats replaces the document with a two-line summary of it. It joins the
same family: -v wins over it as it wins over the name lists, and --json-out
takes precedence over it, so asking for both writes the file and prints the
document as usual. --json-out describes one document, so it is refused when
several files are named.
--emit-moonbit replaces the document with a MoonBit type for it: a struct for
every object the sample holds, holding the members it showed with the types
they showed, each struct deriving FromJson and ToJson. The name after it is
the name of the root type, and it is Root when no name is given. A name has
to start with an upper case letter and go on with letters, digits and
underscores, and the names the generated code is itself written with (Int,
Double, String, Bool, Json and Array) are refused as well: a type named after
one of them would be a type made of itself.
The document the type is read off is the one the run prepared, so --sort-keys
decides the order the fields come out in, --select and the prunings decide
what there is to read, and --trim-strings decides what the strings look like.
A JSON key that is not a name MoonBit can spell is written as close to one as
the language allows, with the key in a comment beside the field: the derived
decoder reads the field name, so the mangled name is what the pasted type
will look for, and the comment is what says which key it stands for.
What a sample cannot say is not guessed at. A member the document holds as
null, or holds in one record and not in the next, is left as a Json or made
optional rather than given a type the sample does not show. --json-out, --ai,
--paths and --keys-only each answer with something else in place of the
document, so none of them can be asked for with --emit-moonbit; --stats gives
way to it, and -v wins over it as it wins over the name lists.
--schema <path> checks the input against a JSON Schema instead of printing
it, and is read with -v, which is the flag that asks for a verdict rather
than a document. The dialect read is the part of the one at json-schema.org
that a document of data is described with: type, required, properties, items,
enum, minimum, maximum, minLength, maxLength and pattern. Every other keyword
is passed over rather than refused, so a schema written for a full validator
can be handed to this one and the part of it this tool does not read is
simply not enforced.
A document that does not match is reported with every place it disagrees,
one to a line, and the run exits 1. A schema this tool cannot read is a
mistake in the command line rather than in the document: it is named, the
keyword to fix is quoted under it, and the run exits 2 without looking at
any input. Under --jsonl every record is checked on its own and reported
with the line it was written on.
--flatten collapses every nested object into dotted keys, and --unflatten
expands them again. The two are inverses of each other, so only one of them
may be given. A document that names one path two ways, with a key "a.b"
beside a key "a", has no flattened or unflattened form and is refused with
both keys named.
--prune-null drops every object member whose value is null, and --prune-empty
drops every empty object and every empty array, then the containers left empty
by that in turn. The two are independent and may be given together, in which
case the nulls go first, so a member left holding {} is dropped as well.
--prune-null keeps every item of an array: a null in an object is a member with
no value, while a null in an array is a value in a place, and dropping it would
renumber the items after it.
--select, --sort-by and --unique rewrite the document rather than lay it out,
and each answers one question about what it should hold. At most one of the
three may be given: two of them are two answers to the same question, and
neither is more nearly right than the other. They run before everything else
that removes or reshapes values, so --select decides what --prune-null,
--prune-empty, --flatten and --unflatten then see. Deduplicating is the one
of the three that compares values with one another, and it does so through
the tidying the run asked for, so --trim-strings and --sort-keys share in
deciding what counts as the same item there.
--select keeps the top-level fields its comma-separated list names, in the
order the list gives them, and drops the rest. A name the object does not
have is skipped rather than reported, so one selection works across a folder
of documents whose fields have drifted apart.
--sort-by sorts an array by the value its path names inside each item. The
document may be an array itself or an object holding exactly one, which is
sorted where it sits; an object holding several is refused, since there is no
telling which was meant. Numbers are ordered as numbers and strings by code
point, and values of different kinds are ordered by kind, as jq orders them:
null, false, true, numbers, strings, arrays, objects. Two arrays, and two
objects, are equal and keep the order they were written in. A record whose
path reaches nothing is sorted as though the field held null, which puts it
before the records that have a value there. The sort is stable.
--unique drops the items that repeat one already seen, keeping the first.
What is compared is the item as the run would print it, so --sort-keys makes
two objects whose members were written in a different order one item, and
--trim-strings makes " a" and "a" one item. With no path the whole item is
compared; with a path it is the value the path names that is. An item whose
path reaches nothing is kept, since it has no value to be compared.
--jsonl reads the input as JSON Lines: one document per line, with blank
lines skipped. Each record is handled on its own, so the other options apply
to every one of them in turn. A record that is not valid JSON is reported
with its line in the file and the rest are still printed; the run exits 1 if
any record failed. It describes many documents at once, so it is refused
with --json-out and with --ai.
--ai asks an OpenAI-compatible chat-completions API for a review of the
formatted document. The endpoint, the model and the key are taken from
--ai-base-url, --model and MOONJSON_AI_API_KEY, each falling back in turn
to MOONJSON_AI_BASE_URL, MOONJSON_AI_MODEL and DEEPSEEK_API_KEY, and then
to DeepSeek's own. There is no --api-key: a key on a command line is a key
in the shell history and in the process list.
--model and --ai-base-url describe that one request, so without --ai they
are read, accepted and ignored.
--moon-deps reads the input as a MoonBit module manifest and prints the
tree of modules it depends on, each dependency read in turn from the
.mooncakes directory beside the manifest. Both manifest forms are read:
moon.mod.json, and the moon.mod whose dependencies sit in an import block.
A dependency that has not been downloaded is shown with nothing under it,
and a module required at more than one version, or a circular dependency,
is reported beneath the tree. The tree is the whole of the output, so no
other option is consulted; --ai reviews the document instead, so under it
--moon-deps does nothing.
Exit codes:
0 success
1 the input could not be read, or is not valid JSON
2 the command line was invalid
3 the input was valid but the AI review failedmoonjson-toolkit -f a.json -f broken.json -f c.json --continue-on-error
# ==> a.json <==
# {
# "a": 1
# }
# ==> c.json <==
# {
# "c": 3
# }
#
# Summary: 2 passed, 1 failed
# failed: broken.json
# error: broken.json is not valid JSON
# line 1, column 8: expected opening quote
# {"a":1,}
# ^error: <stdin> cannot be flattened
conflicting keys: "a" and "a.b"| Code | Meaning |
|---|---|
| 0 | the input was formatted, or validated successfully |
| 1 | the input could not be read, is not valid JSON, or does not match the schema |
| 2 | the command line itself was invalid |
| 3 | the input was valid, but the AI review could not be produced |
moon run cmd/main -- -f test.json{
"name": "moonjson-toolkit",
"version": 2,
"stable": true,
"tags": [
"json",
"cli",
"formatter"
],
"owner": {
"name": "MoonBit",
"contact": {
"email": "dev@moonbitlang.com",
"active": true
}
},
"dependencies": [
{
"name": "parsec",
"version": "0.1.3"
},
{
"name": "async",
"version": "0.22.1"
},
{
"name": "x",
"version": "0.5.5"
}
],
"settings": {
"indent": 2,
"theme": null
}
}moon run cmd/main -- -f a.json -f b.json
# ==> a.json <==
# {
# "a": 1
# }
# ==> b.json <==
# {
# "b": 2
# }moon run cmd/main -- -f a.json -f c.json -f b.json
# ==> a.json <==
# {
# "a": 1
# }
# ==> b.json <==
# {
# "b": 2
# }
# error: c.json is not valid JSON
# line 1, column 8: expected opening quote
# {"c":3,}
# ^printf '{"a":1}\n\n{"b":[2]}\n' | moon run cmd/main -- --jsonl
# {
# "a": 1
# }
# {
# "b": [
# 2
# ]
# }printf '{"a":1}\n{"b":1,}\n{"c":3}\n' | moon run cmd/main -- --jsonl -c
# {"a":1}
# {"c":3}
# error: <stdin> is not valid JSON
# line 2, column 8: expected opening quote
# {"b":1,}
# ^printf '{"name":" moon ","note":" a b "}' | moon run cmd/main -- --trim-strings -c
# {"name":"moon","note":"a b"}printf '{"a":{"b":{"c":1}},"d":[{"e":{"f":2}}]}' | moon run cmd/main -- --flatten -c
# {"a.b.c":1,"d":[{"e.f":2}]}printf '{"a.b":1,"a.c":{"d":2}}' | moon run cmd/main -- --unflatten -c
# {"a":{"b":1,"c":{"d":2}}}printf '{"a":{"b":1},"a.b":2}' | moon run cmd/main -- --flatten -c
# error: <stdin> cannot be flattened
# conflicting keys: "a" and "a.b"printf '{"a":null,"b":{},"c":[1,null],"d":{"e":null}}' | moon run cmd/main -- --prune-null --prune-empty -c
# {"c":[1,null]}echo '{"a":[1,2]}' | moon run cmd/main -- -i4echo '{"b":{"z":1,"a":2},"a":[{"y":3,"x":4}]}' | moon run cmd/main -- -cS
# {"a":[{"x":4,"y":3}],"b":{"a":2,"z":1}}moon run cmd/main -- -v -f test.json
# test.json: valid JSONmoon run cmd/main -- -v --json-out stats.json -f test.json
# test.json: valid JSON
# statistics written to stats.jsonprintf '{"a":1,}' | moon run cmd/main --
# error: <stdin> is not valid JSON
# line 1, column 8: expected opening quote
# {"a":1,}
# ^printf '[[[[]]]]' | moon run cmd/main -- --max-depth 3
# error: <stdin> is not valid JSON
# line 1, column 4: nesting depth exceeds limit 3
# [[[[]]]]
# ^echo '{"name":"x","tags":["a","b"],"owner":{"email":"e"}}' | moon run cmd/main -- --paths
# name
# tags
# tags[0]
# tags[1]
# owner
# owner.emailecho '{"name":"x","tags":["a","b"],"owner":{"email":"e"}}' | moon run cmd/main -- --keys-only
# name
# tags
# owner
# owner.emailprintf '{"a":1,"a":2}' | moon run cmd/main --
# error: <stdin> is not valid JSON
# line 1, column 8: duplicate object key "a" (first defined at line 1, column 2)
# {"a":1,"a":2}
# ^moon run cmd/main -- -f test.json --json-out stats.json{
"total_keys": 19,
"total_nodes": 26,
"max_depth": 4,
"type_counts": [
{ "kind": "null", "count": 1 },
{ "kind": "boolean", "count": 2 },
{ "kind": "number", "count": 2 },
{ "kind": "string", "count": 12 },
{ "kind": "array", "count": 2 },
{ "kind": "object", "count": 7 }
],
"key_counts": [
{ "name": "name", "count": 0 },
{ "name": "version", "count": 0 },
{ "name": "stable", "count": 0 },
{ "name": "tags", "count": 0 },
{ "name": "owner", "count": 4 },
{ "name": "dependencies", "count": 6 },
{ "name": "settings", "count": 2 }
],
"depth_counts": [
{ "depth": 1, "count": 1 },
{ "depth": 2, "count": 7 },
{ "depth": 3, "count": 10 },
{ "depth": 4, "count": 8 }
]
}moon run cmd/main -- --stats -f test.json
# keys: 19 nodes: 26 depth: 4
# type counts: null=1 boolean=2 number=2 string=12 array=2 object=7cat > person.schema.json <<'EOF'
{
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "integer", "minimum": 1 },
"name": { "type": "string", "minLength": 1, "maxLength": 20 },
"email": { "type": "string", "pattern": "^[^@]+@[^@]+$" }
}
}
EOF
printf '{"id":7,"name":"Moon","email":"moon@example.com"}' \
| moon run cmd/main -- -v --schema person.schema.json
# <stdin>: valid JSON, and it matches the schemaprintf '{"id":0,"name":"","extra":true}' \
| moon run cmd/main -- -v --schema person.schema.json
# error: <stdin> does not match the schema
# id: it is less than the minimum 1
# name: it is 0 characters long, and the schema asks for at least 1cat > order.schema.json <<'EOF'
{
"properties": {
"customer": {
"required": ["name"],
"properties": { "name": { "type": "string" } }
},
"lines": { "items": { "properties": { "qty": { "type": "integer" } } } }
}
}
EOF
printf '{"customer":{},"lines":[{"qty":2},{"qty":"three"}]}' \
| moon run cmd/main -- -v --schema order.schema.json
# error: <stdin> does not match the schema
# customer: required member "name" is missing
# lines[1].qty: it is a string where the schema expects an integercat > loose.schema.json <<'EOF'
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "A person",
"type": "object",
"additionalProperties": false,
"anyOf": [{ "required": ["id"] }, { "required": ["name"] }]
}
EOF
printf '{"id":1,"nickname":"Moon"}' \
| moon run cmd/main -- -v --schema loose.schema.json
# <stdin>: valid JSON, and it matches the schemaprintf '{"type":"object","properties":{"id":{"type":"int"}}}' > broken.schema.json
printf '{"id":1}' | moon run cmd/main -- -v --schema broken.schema.json
# error: the schema broken.schema.json is not one this tool can read
# properties.id.type: "int" is not a JSON type; the types are object, array, string, number, integer, boolean and nullprintf '{"id":1,"name":"Moon"}\n\n{"id":"two","name":"Moon"}\n' \
| moon run cmd/main -- --jsonl -v --schema person.schema.json
# error: <stdin> line 3 does not match the schema
# id: it is a string where the schema expects an integerprintf '{"id":1,"name":"Moon"}\n{"id":2,"name":"JSON"}\n' \
| moon run cmd/main -- --jsonl -v --schema person.schema.json
# <stdin>: valid JSON, and every record matches the schemaprintf '{"name":"moon","version":"0.1.0","tags":["json"],"debug":true}' \
| moon run cmd/main -- --select name,tags -c
# {"name":"moon","tags":["json"]}printf '{"name":"moon","private":true}' | moon run cmd/main -- --select name,version -c
# {"name":"moon"}printf '[{"n":3,"tag":"c"},{"n":1,"tag":"a"},{"n":2,"tag":"b"}]' \
| moon run cmd/main -- --sort-by n -c
# [{"n":1,"tag":"a"},{"n":2,"tag":"b"},{"n":3,"tag":"c"}]printf '{"records":[{"user":{"age":30}},{"user":{"age":25}}]}' \
| moon run cmd/main -- --sort-by user.age -c
# {"records":[{"user":{"age":25}},{"user":{"age":30}}]}printf '[{"k":"9"},{"k":10},{"k":2},{"k":"3"}]' | moon run cmd/main -- --sort-by k -c
# [{"k":2},{"k":10},{"k":"3"},{"k":"9"}]printf '[{"n":2},{"other":1},{"n":1}]' | moon run cmd/main -- --sort-by n -c
# [{"other":1},{"n":1},{"n":2}]printf '[3,1,2]' | moon run cmd/main -- --sort-by '' -c
# [1,2,3]printf '[{"id":1,"v":"a"},{"id":2,"v":"b"},{"id":1,"v":"c"}]' \
| moon run cmd/main -- --unique id -c
# [{"id":1,"v":"a"},{"id":2,"v":"b"}]printf '["a","b","a"]' | moon run cmd/main -- --unique -c
# ["a","b"]printf '[{"a":1,"b":2},{"b":2,"a":1}]' | moon run cmd/main -- --unique --sort-keys -c
# [{"a":1,"b":2}]printf '{"name":"moon","note":null,"tags":[]}' \
| moon run cmd/main -- --select name,note,tags --prune-null -c
# {"name":"moon","tags":[]}printf '{"name":"moon","note":null,"tags":[]}' \
| moon run cmd/main -- --select name,note,tags --prune-null --prune-empty -c
# {"name":"moon"}printf '[1,2]' | moon run cmd/main -- --select a --unique -c
# error: --select and --unique each rewrite the document, so at most one of them can be given
# run 'moonjson-toolkit --help' to see the available optionsprintf '{"a":1}' | moon run cmd/main -- --sort-by a
# error: <stdin> cannot be sorted
# --sort-by sorts an array, and this document is an object with no array in itprintf '{"name":"moon","version":1,"tags":["json"],"meta":{"private":true}}' \
| moon run cmd/main -- --emit-moonbit
# /// A MoonBit type for the shape of a JSON document, generated by
# /// moonjson-toolkit --emit-moonbit.
# ///
# /// Paste it into a package that imports "moonbitlang/core/json": that is
# /// where FromJson and ToJson come from. The `extend` lines under each struct
# /// keep the derived methods from being promoted implicitly, which the
# /// compiler reports as deprecated.
# ///
# /// A field is named after the JSON key it holds. A key that is not a name
# /// moonbit can spell appears mangled, with the key written beside it.
#
# pub struct Root {
# name : String
# version : Int
# tags : Array[String]
# meta : RootMeta
# } derive(FromJson, ToJson)
#
# pub extend Root with FromJson::{from_json}
# pub extend Root with ToJson::{to_json}
#
# pub struct RootMeta {
# private : Bool
# } derive(FromJson, ToJson)
#
# pub extend RootMeta with FromJson::{from_json}
# pub extend RootMeta with ToJson::{to_json}printf '[{"id":1,"tag":"a"},{"id":2}]' \
| moon run cmd/main -- --emit-moonbit Payload | sed -n '/^pub /,$p'
# pub type Payload = Array[PayloadItem]
#
# pub struct PayloadItem {
# id : Int
# tag : String?
# } derive(FromJson, ToJson)
#
# pub extend PayloadItem with FromJson::{from_json}
# pub extend PayloadItem with ToJson::{to_json}printf '{"a":1}' | moon run cmd/main -- --emit-moonbit payload
# error: --emit-moonbit takes the name of the root type, and "payload" starts with a lower case letter (a type name starts with an upper case one)
# run 'moonjson-toolkit --help' to see the available optionsprintf '{"a":1,"b":1.5,"c":2147483648,"d":[]}' \
| moon run cmd/main -- --emit-moonbit | sed -n '/^pub /,$p'
# pub struct Root {
# a : Int
# b : Double
# c : Double
# d : Array[Json]
# } derive(FromJson, ToJson)
#
# pub extend Root with FromJson::{from_json}
# pub extend Root with ToJson::{to_json}printf '[{"id":1,"n":null},{"id":2,"extra":"x"}]' \
| moon run cmd/main -- --emit-moonbit | sed -n '/^pub /,$p'
# pub type Root = Array[RootItem]
#
# pub struct RootItem {
# id : Int
# n : Json?
# extra : String?
# } derive(FromJson, ToJson)
#
# pub extend RootItem with FromJson::{from_json}
# pub extend RootItem with ToJson::{to_json}printf '{"user-name":"a","2fa":true,"type":"t"}' \
| moon run cmd/main -- --emit-moonbit | sed -n '/^pub /,$p'
# pub struct Root {
# user_name : String // "user-name"
# _2fa : Bool // "2fa"
# type_ : String // "type"
# } derive(FromJson, ToJson)
#
# pub extend Root with FromJson::{from_json}
# pub extend Root with ToJson::{to_json}printf '{"b":1,"a":{"z":1}}' \
| moon run cmd/main -- --sort-keys --emit-moonbit | sed -n '/^pub /,$p'
# pub struct Root {
# a : RootA
# b : Int
# } derive(FromJson, ToJson)
#
# pub extend Root with FromJson::{from_json}
# pub extend Root with ToJson::{to_json}
#
# pub struct RootA {
# z : Int
# } derive(FromJson, ToJson)
#
# pub extend RootA with FromJson::{from_json}
# pub extend RootA with ToJson::{to_json}cd frontend
# 1. Produce the data the page reads.
moon run ../cmd/main -- -f ../test.json --json-out public/stats.json
# 2. Build the browser bundle.
warren build --browser-entry main
# 3. Serve dist/ over HTTP and open it.
python3 -m http.server 8000 --directory distexport DEEPSEEK_API_KEY=sk-...
moon run cmd/main -- --ai -f test.json| Setting | Flag | Environment variable | Default |
|---|---|---|---|
| Endpoint | --ai-base-url <url> | MOONJSON_AI_BASE_URL | DeepSeek's |
| Model | --model <name> | MOONJSON_AI_MODEL | deepseek-flash |
| API key | — | MOONJSON_AI_API_KEY, else DEEPSEEK_API_KEY | — |
| Provider | --ai-base-url | --model |
|---|---|---|
| DeepSeek (the default) | https://api.deepseek.com/chat/completions | deepseek-flash |
| Kimi (Moonshot) | https://api.moonshot.cn/v1/chat/completions | kimi-k3 |
| 智谱 GLM | https://open.bigmodel.cn/api/paas/v4/chat/completions | glm-4-flash |
| OpenAI | https://api.openai.com/v1/chat/completions | gpt-4o-mini |
| Ollama (local) | http://localhost:11434/v1/chat/completions | llama3.1 |
# DeepSeek, which needs neither flag: this is the default
MOONJSON_AI_API_KEY=sk-... moon run cmd/main -- --ai -f test.json
# Kimi
MOONJSON_AI_API_KEY=sk-... moon run cmd/main -- --ai \
--ai-base-url https://api.moonshot.cn/v1/chat/completions --model kimi-k3 -f test.json
# 智谱 GLM
MOONJSON_AI_API_KEY=... moon run cmd/main -- --ai \
--ai-base-url https://open.bigmodel.cn/api/paas/v4/chat/completions --model glm-4-flash -f test.json
# OpenAI
MOONJSON_AI_API_KEY=sk-... moon run cmd/main -- --ai \
--ai-base-url https://api.openai.com/v1/chat/completions --model gpt-4o-mini -f test.json
# Ollama, which asks for no key of its own: the tool still wants one to start
# a review, so any placeholder will do
MOONJSON_AI_API_KEY=ollama moon run cmd/main -- --ai \
--ai-base-url http://localhost:11434/v1/chat/completions --model llama3.1 -f test.jsonmoon run cmd/main -- --moon-deps -f moon.mod
# BigSaltyMan/moonjson-toolkit@0.1.0
# ├── Nanaloveyuki/parsec@0.1.3
# ├── oboard/mio@0.5.4
# │ ├── moonbitlang/x@0.5.5
# │ ├── moonbitlang/async@0.22.1
# │ └── bikallem/compress@0.3.4
# │ ├── moonbitlang/async@0.22.1
# │ └── bikallem/blit@0.2.2
# ├── moonbitlang/x@0.5.5
# └── moonbitlang/async@0.22.1
#
# warning: 2 versions of moonbitlang/x are required
# 0.5.5 by BigSaltyMan/moonjson-toolkit@0.1.0
# 0.4.50 by oboard/mio@0.5.4
#
# warning: 3 versions of moonbitlang/async are required
# 0.22.1 by BigSaltyMan/moonjson-toolkit@0.1.0
# 0.20.6 by oboard/mio@0.5.4
# 0.16.7 by bikallem/compress@0.3.4echo '{"name":"myproject","version":"0.1.0","deps":{"moonbitlang/x":"0.5.5"}}' \
| moon run cmd/main -- --moon-deps
# myproject@0.1.0
# └── moonbitlang/x@0.5.5tail -6 moon.mod
# import {
# "Nanaloveyuki/parsec@0.1.3",
# "oboard/mio@0.5.4",
# "moonbitlang/x@0.5.5",
# "moonbitlang/async@0.22.1",
# }# Two manifests in a scratch directory, each asking for the other.
mkdir -p /tmp/cycle/.mooncakes/b
printf 'name = "b"\n\nversion = "1.0.0"\n\nimport {\n "a@1.0.0",\n}\n' > /tmp/cycle/.mooncakes/b/moon.mod
printf 'name = "a"\n\nversion = "1.0.0"\n\nimport {\n "b@1.0.0",\n}\n' > /tmp/cycle/moon.mod
moon run cmd/main -- --moon-deps -f /tmp/cycle/moon.mod
# a@1.0.0
# └── b@1.0.0
# └── a@1.0.0
#
# warning: circular dependency detected
# a → b → amoonjson-toolkit/
├── moon.mod module metadata and dependencies
├── moon.pkg the library package and its imports
├── formatter.mbt pretty-printing
├── flatten.mbt dotted keys out of nesting, and back again
├── prune.mbt dropping null members and empty containers
├── transform.mbt selecting, sorting and deduplicating
├── emit.mbt a MoonBit type read off the shape of a document
├── schema.mbt checking a document against a JSON Schema
├── pattern.mbt the regular expressions a schema pattern is read with
├── jsonl.mbt splitting a JSON Lines input into records
├── diagnostics.mbt offsets to line/column, rendered error snippets
├── color.mbt the colour decision and the escape wrapping
├── cli.mbt argument parsing and usage text
├── parser.mbt parsing and file/standard-input reading
├── paths.mbt the path of every value, and of every object member
├── ai.mbt the AI request, its settings and its response handling
├── moondeps.mbt module manifests, read into a dependency tree
├── stats.mbt the statistics model and its JSON form
├── runner.mbt one run of the tool, and the exit codes
├── cmd/main/ the process entry point
└── frontend/ the Rabbita dashboard
├── main/main.mbt the app: model, update and view
└── public/ index.html, styles.css, charts.js, echarts.min.jsmoon check --target native # type-check
moon test --target native # 305 tests
moon fmt # format
cd frontend
moon test --target js # 4 testsmoon coverage analyze -- -f summaryai.mbt: 80/90
cli.mbt: 160/164
cmd/main/main.mbt: 0/14
color.mbt: 41/43
diagnostics.mbt: 81/98
emit.mbt: 162/164
flatten.mbt: 100/102
parser.mbt: 261/276
pattern.mbt: 283/290
runner.mbt: 330/358
schema.mbt: 345/351
transform.mbt: 141/143
Total: 2558/2667| Module | Coverage |
|---|---|
| formatter.mbt, jsonl.mbt, moondeps.mbt, paths.mbt, prune.mbt, stats.mbt | 100% |
| emit.mbt | 98.8% |
| transform.mbt | 98.6% |
| schema.mbt | 98.3% |
| flatten.mbt | 98.0% |
| cli.mbt | 97.6% |
| pattern.mbt | 97.6% |
| color.mbt | 95.3% |
| parser.mbt | 94.6% |
| runner.mbt | 92.2% |
| ai.mbt | 88.9% |
| diagnostics.mbt | 82.7% |
| cmd/main/main.mbt | 0% |
bench/run.shmoonjson-toolkit 0.1.0
best of 3 runs, times in seconds
file size format validate
small.json 982 B 0.002 s 0.002 s
medium.json 1.1 MB 0.042 s 0.035 s
large.json 9.6 MB 0.304 s 0.248 sbench/batch.shmoonjson-toolkit 0.1.0
best of 7 runs, times in seconds
input files separate batch
small.json 200 0.735 s 0.039 s| Piece | Used for |
|---|---|
| MoonBit | the whole project |
| Nanaloveyuki/parsec | parser combinators, with the JSON grammar in parsec/json |
| oboard/mio | the HTTP request behind --ai |
| moonbitlang/async | async runtime, timeouts, file and standard-stream IO |
| moonbitlang/x | system helpers |
| Rabbita | the web dashboard, compiled to JavaScript |
| Apache ECharts | the bar and pie charts |
| Dependency | Version | License |
|---|---|---|
| Nanaloveyuki/parsec | 0.1.3 | Apache-2.0 |
| oboard/mio | 0.5.4 | Apache-2.0 |
| moonbitlang/async | 0.22.1 | Apache-2.0 |
| moonbitlang/x | 0.5.5 | Apache-2.0 |
| moonbit-community/rabbita | 0.15.2 | Apache-2.0 |
pub(all) struct CliOptions {
files : Array[String]
indent : Int
max_depth : Int?
compact : Bool
sort_keys : Bool
trim_strings : Bool
jsonl : Bool
flatten : Bool
unflatten : Bool
prune_null : Bool
prune_empty : Bool
select : String?
sort_by : String?
unique : String?
paths : Bool
keys_only : Bool
stats : Bool
emit_moonbit : String?
validate : Bool
schema : String?
help : Bool
version : Bool
ai : Bool
model : String?
ai_base_url : String?
moon_deps : Bool
fail_fast : Bool
continue_on_error : Bool
json_out : String?
no_color : Bool
}pub struct JsonDiagnostic {
offset : Int
line : Int
column : Int
message : String
snippet : String
}keys: 6 nodes: 9 depth: 4
type counts: object=3 array=1 string=3 number=2pub(all) struct RunResult {
out : String
err : String
code : Int
}pub(all) struct SchemaError {
path : String
message : String
}async fn analyze(json_text : String, endpoint : String, model : String, api_key : String) -> Result[String, String]let api_key_variable : Stringasync fn build_dep_tree_from(root : ModuleInfo, lookup : async (String) -> Result[ModuleInfo, String]) -> DepNodefn clamp_indent(indent : Int) -> Int{"name": "x", "tags": ["a"], "nested": {"a": {"b": 1}}}fn color_allowed(no_color_var : String?, term : String?, output_is_terminal : Bool?) -> Boollet deepseek_endpoint : Stringlet deepseek_model : Stringlet default_indent : Intlet default_root_name : Stringfn dependency_directory(source : String) -> Stringlet endpoint_variable : Stringfn env_allows_color(no_color_var : String?, term : String?) -> Boolfn escape_json_string(value : String) -> Stringlet exit_ai_error : Intlet fallback_api_key_variable : Stringasync fn is_tty() -> Boolfn line_text(input : String, line : Int) -> Stringlet max_prompt_chars : Intfn non_empty_env(name : String) -> String?fn offset_to_line_col(input : String, offset : Int) -> (Int, Int)fn pattern_matches(pattern : String, text : String) -> Result[Bool, String]let program_name : Stringfn quote_for_message(value : String) -> Stringasync fn read_file(path : String) -> Result[String, String]async fn read_stdin() -> Result[String, String]fn red(text : String) -> Stringlet request_timeout_ms : Intfn resolve_api_key(newer : String?, older : String?) -> Result[String, String]fn resolve_endpoint(cli_url : String?, environment_url : String?) -> Stringfn resolve_model(cli_model : String?, environment_model : String?) -> Stringfn set_color_enabled(enabled : Bool) -> Unitlet standard_max_depth : Intlet standard_max_input_chars : Intfn type_name_problem(name : String) -> String?conflicting keys: "a" and "a.b"let version : Stringfn yellow(text : String) -> StringInstall
Download zipA JSON toolkit for MoonBit: format, validate, diagnose, analyse and visualise JSON documents.
Dependencies