Yet another command-line tool for converting JSON strings into Golang struct definitions. It’s designed to be flexible, allowing for JSON input from both files and pipes, and it handles pointer types for nested structures when specified.
- Supports both inline and outline Golang structs definitions
- Support options that change the interpretation of json
null - Receives JSON strings directly or from a file
- Outputs formatted Golang structs use
gofmt - Option to define struct and array fields as pointers
- Flexible with file extensions (no need for
.json) - Option to disable the pager for larger outputs
You can install kirke using Homebrew (it also installs the Bash and Zsh completions):
brew install magicdrive/tap/kirke
brew upgrade kirke # later, to update it
Or clone the repository and build the binary (needs Go, see go.mod; the binary is
build/kirke):
git clone https://github.com/magicdrive/kirke.git
cd kirke
make build
Pre-built archives for macOS (amd64, arm64), Linux (386, amd64, arm64) and Windows (386, amd64,
arm64) are on the Releases page, with
kirke_checksums.txt (SHA-256) to check them against.
Download the script, read it if you like, then run it with sh:
curl -fsSLO https://raw.githubusercontent.com/magicdrive/kirke/main/install.sh sh install.sh # the latest release, into ~/.local/bin "$HOME/.local/bin/kirke" --version # prints: kirke version vX.Y.Z
sh install.sh # install the latest release sh install.sh --version v1.3.0 # install a given release (older ones too) sh install.sh --upgrade # switch to the latest stable release sh install.sh --upgrade --version v1.3.0 # switch to a given release (also a downgrade) sh install.sh --dir "$HOME/.local/bin" # choose the directory (this is the default) sh install.sh --help
- Where:
$HOME/.local/bin/kirkeby default;--dir DIRinstallsDIR/kirkeand createsDIRif needed (spaces inDIRare fine). It never usessudoand never changesPATHor shell start-up files: if the script says the directory is not inPATH, add it, e.g.export PATH="$HOME/.local/bin:$PATH"in~/.profile. For a system directory such as/usr/local/bin, run it as a user who can write there (it does not ask forsudoitself). - Versions: without
--versionit installs the latest stable release (pre-releases are not picked).--versiontakes a release tag,v1.3.0or1.3.0, newer or older than the installed one; a version that does not exist is an error, never another version.--upgradeis the same run, named for what you mean. Running it again with the installed version changes nothing. Every kirke release can be installed (v0.0.1 and later). - Updating: run
sh install.sh --upgradeagain (with the same--dirif you used one). - Checks: the archive must match the SHA-256 listed in the release's checksum file
(
kirke_checksums.txt); a missing or malformed checksum file stops the install. Only thekirkebinary is taken out of the archive, after its entries have been checked (no absolute paths,.., links or devices). The new binary must run and report the version asked for before it replaces the old one in a single rename: a failed or interrupted run leaves the installedkirkeas it was. - Security: every request starts at github.com, over https, and redirects (GitHub sends the
downloads to its asset host) are followed only to https. The checksum file comes from the same GitHub release as the archive, so it catches a
broken or altered download, but not a release that was itself replaced on GitHub. For a
stronger check, verify the archive's build provenance with the
GitHub CLI before you trust it:
gh attestation verify kirke_<version>_<os>_<arch>.tar.gz -R magicdrive/kirke(releases from v1.3.0 on have these attestations). The command above runs the script from themainbranch; to run a fixed script, download it from a release tag instead (https://raw.githubusercontent.com/magicdrive/kirke/<tag>/install.sh; the tags from v1.5.0 on have it). Piping it straight intosh(curl ... | sh) also works, but then you run what you have not seen. - Homebrew: the script refuses Homebrew's directories (its prefix, e.g.
/opt/homebrew, or/usr/localon an Intel Mac) and does not replace a symbolic link such as one Homebrew made. Keep one way of installing: with Homebrew, upgrade withbrew upgrade kirke; the script warns if anotherkirkecomes first inPATH. - Uninstall: delete the file:
rm "$HOME/.local/bin/kirke"(orDIR/kirke). - Needs: a POSIX
sh,curl(or GNU Wget 1.17+; BusyBox wget is refused because it does not check certificates, and wget2 is not supported),tar,gzip, andsha256sum,shasumoropenssl. A proxy set inhttps_proxyis used.INSTALLER_TIMEOUT(seconds, default 60) is how long it waits for a connection or more data. - Windows: the script does not run on Windows (Git Bash, MSYS2 and Cygwin are refused). Use
the
kirke_<version>_windows_<arch>.tar.gzof the Releases page (it holdskirke.exe). Inside WSL, the Linux instructions apply. - Troubleshooting: not writable — choose a directory of your own with
--dir; not in PATH — see above; does not run in DIR (noexec) — choose a directory on another file system; SHA-256 mismatch or rejected — nothing was installed; try again later and report it if it persists; release not found — check the version on the Releases page; inside the Homebrew prefix — usebrew upgrade kirke, or another--dir.
curl -s https://api.github.com/repos/magicdrive/kirke | kirke
git clone https://github.com/magicdrive/kirke.git
cd kirke
source ./misc/completions/kirke-completion.sh
kirke [OPTIONS] [ARGUMENTS]
Receives a JSON string and outputs it as a Golang struct definition.
The JSON is taken from the first of: standard input with --pipe; --json; --file; standard input when it is not a terminal (a pipe or a redirected file).
Without any JSON and without arguments, kirke prints the help message.
| Option | Description | Default |
|---|---|---|
-h, --help |
Show this help message and exit | |
-v, --version |
Show version and exit | |
-n, --name <root-name> |
Specify the name of the root struct. Converts to camel case automatically. | AutoGenerated |
-f, --file <json-file> |
Specify the input file containing JSON. Accepts files without .json extension. |
|
-a, --null-as <null-type-name> |
Specify the null-type name. Used to replace null type from json. |
any |
-j, --json <json-string> |
Specify the JSON string to be converted to a struct. | |
-p, --pipe |
Receive JSON from a pipe instead of a direct argument above all else. | false |
--pointer-struct, --pointer <on|off> |
Define nested struct fields and struct array elements as pointers (outline only). | off |
--auto-pager, --pager <on|off> |
on: use a pager when the output is taller than the terminal. off: never. |
on |
-o, --outline |
Output nested structs as separate type definitions (outline mode). | outline mode |
-i, --inline |
Output nested structs inline (inline mode). Cannot be used with --outline. |
false |
<root-name>: Used as the name of the root struct in the output. Automatically converted to camel case.<json-file>: File containing JSON data. Can be read even if the extension is not.json.<json-string>: Direct JSON string input.<null-type-name>: The type name used to replacenulltype from json.
| Environment | Description | Default |
|---|---|---|
KIRKE_DEFAULT_ROOT_NAME |
Specified default used <root-name>. |
AutoGenerated |
KIRKE_DEFAULT_NULL_AS |
Specified default used <null-type-name>. |
any |
KIRKE_DEFAULT_OUTPUT_MODE |
Output mode used without --inline and --outline: inline, or outline for any other value. |
outline |
KIRKE_DEFAULT_POINTER_STRUCT_MODE |
Default of --pointer-struct. Only on and off are valid; other values are ignored. |
off |
KIRKE_DEFAULT_AUTO_PAGER_MODE |
Default of --auto-pager. Only on and off are valid; other values are ignored. |
on |
Every JSON value is typed so that json.Unmarshal decodes it into the generated struct and json.Marshal encodes it back with the same value (numbers by value, see below), except where noted.
| JSON value | Go type |
|---|---|
string, true/false |
string, bool |
null |
<null-type-name> (any) |
| object | a struct (a pointer with --pointer-struct on, outline only) |
integer that fits in int64 (42, -0) |
int |
larger integer (9223372036854775808) |
*big.Int |
fraction or exponent that float64 holds as written (1.5, 1.0, 0.1, 1e3, 1.2e-4) |
float64 |
fraction or exponent that float64 changes (3.141592653589793238462643383279, 1e400, 1e-400, 4e-324) |
json.Number (keeps the literal) |
| array | a slice of one type for all its elements (below) |
"float64 holds as written" means that the shortest decimal form of its float64 value, which json.Marshal writes, has the value of the JSON literal (1.0 is written 1, 0.1 stays 0.1).
The generated code uses json.Number and json.RawMessage (package encoding/json) and *big.Int (package math/big) without the import declarations.
An array is typed from all of its elements, whatever their order, in both output modes:
- Numbers: the first of
int,*big.Int(all integers),float64(all held as written),json.Numberthat holds every one of them:[1, 2]is[]int,[1, 2.5]is[]float64,[1, 9223372036854775808]is[]*big.Int,[0.1, 1e-400]is[]json.Number. - Strings:
[]string; booleans:[]bool. - Objects: one struct with the keys of all the objects, in the order they first appear. A key that several objects have is typed from all of its values, as array elements are:
[{"v": 1}, {"v": 2.5}, {"w": "a"}]is a slice ofstruct { V float64; W string }. - Arrays: the elements of all of them:
[[1, 2], [3, 4.5]]is[][]float64. nullamong them makes the element a pointer:[1, null]is[]*int,["a", null]is[]*string,[{...}, null]is a slice of struct pointers;*big.Intand slices already are nil fornull. The same holds for a key of merged objects:[{"a": 1}, {"a": null}]givesA *int.- Elements of different kinds (
[1, "a", true],[{...}, 1]):[]interface{}, or[]json.RawMessagewhen one of them is, or holds at any depth, a number float64 changes (interface{}would decode it into a float64). - Only
null, or no element:[]<null-type-name>.
- The value of every number:
1e3comes back as1000and1.0as1(float64,int), the same values.json.Numberandjson.RawMessagekeep the literal as written. kirke does not keep how a number was written in other types. - A key that only some objects of an array have decodes into its zero value for the others, and
json.Marshalwrites that zero value: the generated struct does not tell a missing key from its zero value (nor fromnull, unless the type is a pointer). --null-aswith a type that cannot holdnull(string,int) decodesnullto its zero value.intassumes a 64-bitint: on a 32-bit platform (GOARCH=386,arm) an integer beyond int32 does not decode into it; useint64there.
- A field is named after its key in camel case (
user_idisUserId). A key that does not give a Go identifier keeps its letters, digits and underscores, each run starting with a capital letter, after anXwhen it would start with a digit or be empty ($setisSet,my-keyisMyKey,1stisX1st,_isX); a name that would not be exported, which encoding/json ignores, also starts withX(名前isX名前); when an earlier key of the object gives the same name, a number is appended (a_bandaBareABandAB2). The keys whose camel case already is a name are named first, so a converted key never takes their name ({"_": 1, "x": 2}givesX2andX). The JSON tag always has the key. - A key that appears more than once in one object is one field, typed from all of its values.
- The empty key
""gives an embedded field, as before: encoding/json matches no struct field with it, so its value is not decoded. - A key that encoding/json cannot match with a struct tag (one with a quote, a backslash, a comma, an apostrophe, a control character or a symbol such as an emoji) is an error that names the key, rather than a field that would stay empty. The key
-is taggedjson:"-,". - Outline: a struct is named after its key (an array's element struct after the key and
Item). When that name is already used in the output, by the root struct or by another struct, a number is appended (Meta2); a struct with the same name and the same fields is the same type. The structs that versions before arrays were typed from all elements generated (from an array's first element when it is notnull) are named first, so their names do not change. - Inline: an empty object is
struct{}.
Convert a JSON string to a struct without using a pager and with a specified root name:
kirke -j '{"key": "value"}' --pager off --name MyStruct
Convert JSON data from a specified file to a struct with a specified root name:
kirke -f ./path/to/example.json --name MyExample
Read JSON data from a pipe, use pointers for nested fields, and output as a struct:
echo '{"key": "value"}' | kirke --pointer on
Read JSON data from a pipe, use pointers for nested fields, and output as a struct (-j option is ignored.):
echo '{"key": "value"}' | kirke --pipe --pointer on -j '{"key2": "value2"}'
Read JSON data from a pipe and use *string as the type of the null values:
echo '{"key": "value", "obj": {"address": null} }' | kirke -a "*string"
Copyright (c) 2025 Hiroshi IKEGAMI