Skip to content

x.json5 #

v-json5

JSON5 parsing, decoding and encoding for V.

JSON5 is a superset of JSON: everything that is valid JSON is valid JSON5, and JSON5 additionally allows comments, unquoted object keys, single-quoted strings, trailing commas, hexadecimal and leading- or trailing-dot numbers, a leading + on numbers, and Infinity/NaN.

Contents

  • Install
  • Parse
  • Decode into V types
  • Document access
  • Encode
  • Encode options
  • Custom encoders
  • Supported JSON5 syntax
  • Errors
  • Examples

Install

v install x.json5

Parse

parse returns the dynamic Any tree, and parse_text returns a Doc that also supports path lookup.

import x.json5

fn main() {
    value := json5.parse('{ name: "V", tags: [1, 2], }') or { panic(err) }
    println(value.as_map()['name'] or { json5.null }.string())
    println(value.as_map()['tags'] or { json5.null }.array().len)
}

Decode into V types

decode[T] converts a JSON5 document into T.

import x.json5

struct Config {
    name  string
    port  int = 8080
    ratio f64
}

fn main() {
    cfg := json5.decode[Config]('{ name: "srv", ratio: .5 }') or { panic(err) }
    println(cfg.name)  // srv
    println(cfg.port)  // 8080, the default is kept for a missing key
}

Supported targets:

  • structs, including embedded structs, which are flattened into the parent;
  • enums, by name or by numeric value;
  • map[string]T, []T, [N]T, ?T fields, and all integer and float widths;
  • Any itself, for a partially typed document.

A null value leaves a field at its default. A missing key also keeps the default, so a partially filled document decodes without error.

Field attributes

  • @[skip] ignores the field in both directions;
  • @[json5: 'name'] renames the field, and works on enum values too.

Hooks

A type can take over its own decoding. The hooks are tried in order and the first one defined wins:

  • fn (mut t T) from_json5(value Any);
  • fn (mut t T) from_json5_string(raw string) !;
  • fn (mut t T) from_json5_number(raw string) !;
  • fn (mut t T) from_json5_boolean(raw bool) !;
  • fn (mut t T) from_json5_null().

The string and number hooks receive the original literal source text, which means a hexadecimal literal such as 0x10 reaches the hook intact.

Document access

Doc keeps the parsed tree together with a small path query language.

import x.json5

fn main() {
    doc := json5.parse_text('{ servers: [{ host: "a" }, { host: "b" }] }') or {
        panic(err)
    }
    host := doc.get('servers[1].host') or { panic(err) }
    println(host.string())  // b

    // reflect fills what it can and leaves the rest at the defaults.
    println(doc.reflect[Config]().name)
    // decode reports a type error instead.
    println(doc.decode[Config]() or { panic(err) }.name)
}

Encode

encode[T] writes compact output that is also valid JSON. encode_any writes an Any tree.

import x.json5

struct Point {
    x int
    y int
}

fn main() {
    println(json5.encode(Point{x: 1, y: 2}))
    println(json5.encode(json5.parse('{ a: 1 }') or { panic(err) }))
}

Encode options

encode_with_opts[T] takes an EncodeOpts value.

import x.json5

fn main() {
    opts := json5.EncodeOpts{
        indent:          '  '
        single_quotes:   true
        unquoted_keys:   true
        trailing_commas: true
    }
    println(json5.encode_with_opts(json5.parse('{ a: [1, 2] }') or { panic(err) },
        opts))
}
  • indent: the indent unit; the zero value writes compact output;
  • single_quotes: write strings with ' instead of ";
  • unquoted_keys: write an object key bare when it is a valid identifier;
  • trailing_commas: write a comma after the last array element and member.

Custom encoders

A type can control its own output with to_json5() string. The returned text is written verbatim, which is how a type controls quoting or emits a value that has no V equivalent.

import x.json5

struct Money {
    cents int
}

fn (m Money) to_json5() string {
    return '\$${m.cents / 100}.${m.cents % 100:02}'
}

fn main() {
    println(json5.encode(Money{cents: 1999}))  // $19.99
}

Supported JSON5 syntax

  • // line comments and /* */ block comments;
  • unquoted object keys, restricted to identifiers and reserved words;
  • single-quoted and double-quoted strings;
  • trailing commas in objects and arrays;
  • 0x hexadecimal integers, leading-dot and trailing-dot numbers, a leading + on numbers, and Infinity, -Infinity and NaN;
  • a leading byte order mark;
  • escaped line continuations inside strings;
  • JSON5 escape sequences in strings, including \', \", \v, \0, \xNN and \uNNNN.

Hexadecimal numbers and escape sequences accept both uppercase A-F and lowercase a-f. Escaped UTF-16 surrogate pairs decode to a single Unicode character in strings and quoted keys. Unpaired surrogate escapes report a parse error instead of producing invalid UTF-8. When encoding optional values, present values keep their payload and none becomes null.

Errors

Parsing, decoding and encoding report typed errors that carry a source position:

  • ParseError, for malformed input;
  • TypeError, for a value that does not fit the target type, including an integer that does not fit its width;
  • NameError, for a string that matches no enum value;
  • EnumError, for an out-of-range numeric enum value.

Examples

See the examples directory for complete runnable examples.

Constants #

const null = Any(Null{})

null is the Any value for a missing key, mirroring toml.null.

const default_encode_opts = EncodeOpts{}

default_encode_opts writes compact output that is also valid JSON.

fn decode #

fn decode[T](text string) !T

decode parses the JSON5 document in text and decodes it into T.

Supported targets are structs, enums, map[string]V, arrays (dynamic and fixed), options and all scalar types, plus the Any type itself. Struct fields are matched by name, or by an @[json5: 'name'] attribute when the document key differs; @[skip] leaves a field at its default value. An embedded struct is flattened, so its fields are read from the enclosing object.

A type may customise decoding by defining one of:

  • from_json5(value Any), to decode the whole value
  • from_json5_string(raw string) !, from_json5_number(raw string) ! or from_json5_boolean(value bool), to decode a single scalar

A missing key leaves the field at its default. An explicit null clears an option field and leaves other fields at their default.

fn decode_any #

fn decode_any[T](value Any) !T

decode_any converts a parsed Any into T, applying the same rules as decode without re-parsing.

fn decode_file #

fn decode_file[T](path string) !T

decode_file reads the JSON5 document at path and decodes it into T.

fn encode #

fn encode[T](value T) string

encode writes value as compact JSON, which is also valid JSON5. A type may customise the output by defining to_json5() string.

fn encode_any #

fn encode_any(value Any, opts EncodeOpts) string

encode_any writes an Any tree as JSON5 text.

fn encode_with_opts #

fn encode_with_opts[T](value T, opts EncodeOpts) string

encode_with_opts writes value as JSON5 text shaped by opts.

fn new_doc #

fn new_doc(root Any) Doc

new_doc wraps an already parsed Any tree.

fn new_parser #

fn new_parser(text string) &Parser

new_parser creates a Parser over text.

fn new_scanner #

fn new_scanner(text string) &Scanner

new_scanner creates a Scanner over text, dropping a leading byte order mark.

fn parse #

fn parse(text string) !Any

parse parses the JSON5 document in text and returns its root Any. It is the shortest path from text to a dynamic value.

fn parse_file #

fn parse_file(path string) !Doc

parse_file reads and parses the JSON5 document at path.

fn parse_path #

fn parse_path(path string) ![]PathStep

parse_path splits a lookup path into steps. Quoted segments keep any dots they contain, so a."b.c" addresses the key b.c inside the object a.

The path is walked with an explicit index rather than a state machine, which keeps the "flush the pending key" logic in one place.

fn parse_text #

fn parse_text(text string) !Doc

parse_text parses the JSON5 document in text and returns a Doc.

fn quote_string #

fn quote_string(value string) string

quote_string returns value as a quoted JSON5 string.

fn reindent #

fn reindent(text string) !string

reindent reparses text and writes it back with two-space indentation, which is a convenience for tidying configuration files.

fn resolve_path #

fn resolve_path(value Any, steps []PathStep) Any

resolve_path walks steps through value, returning Null when a step does not resolve.

type Any #

type Any = Null
	| Number
	| Raw
	| bool
	| f64
	| []Any
	| map[string]Any
	| string

Any is the dynamic representation of a parsed JSON5 value. It mirrors the shape of json2.Any, but numbers keep their literal source text so that Infinity, NaN and hexadecimal literals survive a round-trip.

fn (Any) str #

fn (a Any) str() string

str returns Any as JSON5 text.

fn (Any) string #

fn (a Any) string() string

string returns Any as a string. Non-string scalars are converted.

fn (Any) int #

fn (a Any) int() int

int returns Any as an int.

fn (Any) i64 #

fn (a Any) i64() i64

i64 returns Any as an i64.

fn (Any) u64 #

fn (a Any) u64() u64

u64 returns Any as a u64.

fn (Any) f64 #

fn (a Any) f64() f64

f64 returns Any as an f64.

fn (Any) bool #

fn (a Any) bool() bool

bool returns Any as a bool.

fn (Any) array #

fn (a Any) array() []Any

array returns Any as an array. An object is returned as its values, and any other value as a single-element array.

fn (Any) as_map #

fn (a Any) as_map() map[string]Any

as_map returns Any as an object. An array is keyed by its indices and any other value becomes a single-entry object under the key 0.

fn (Any) default_to #

fn (a Any) default_to(value Any) Any

default_to returns value if a is Null.

fn (Any) kind #

fn (a Any) kind() ValueKind

kind returns the JSON5 kind of a.

fn (Any) is_null #

fn (a Any) is_null() bool

is_null reports whether a is the JSON5 null literal.

fn (Any) is_number #

fn (a Any) is_number() bool

is_number reports whether a is a number.

fn (Any) is_string #

fn (a Any) is_string() bool

is_string reports whether a is a string.

fn ([]Any) as_strings #

fn (a []Any) as_strings() []string

as_strings returns the array's elements as strings.

fn (map[string]Any) as_strings #

fn (m map[string]Any) as_strings() map[string]string

as_strings returns the object's values as strings.

fn (map[string]Any) get #

fn (m map[string]Any) get(key string) ?Any

get returns the value stored under key, or none when it is absent.

fn (map[string]Any) has #

fn (m map[string]Any) has(key string) bool

has reports whether m contains key.

enum TokenKind #

enum TokenKind {
	none
	error
	comment
	str
	ident
	number
	bool
	null
	infinity
	nan
	comma = 0x2C // ,
	colon = 0x3A // :
	lsbr  = 0x5B // [
	rsbr  = 0x5D // ]
	lcbr  = 0x7B // {
	rcbr  = 0x7D // }
	eof
}

TokenKind identifies the kind of a JSON5 token.

enum ValueKind #

enum ValueKind {
	bool
	number
	string
	null
	array
	object
}

ValueKind names the JSON5 kinds of value.

fn (ValueKind) str #

fn (k ValueKind) str() string

str returns the JSON5 kind name, for error messages.

struct Doc #

struct Doc {
pub:
	root Any
}

Doc is a parsed JSON5 document. Use value, value_opt or decode to read from it.

fn (Doc) to_any #

fn (d Doc) to_any() Any

to_any returns the document root as a dynamic value.

fn (Doc) str #

fn (d Doc) str() string

str returns the document rendered as JSON5 text.

fn (Doc) value #

fn (d Doc) value(key string) Any

value queries a value with a small path syntax: dots separate object keys, brackets index arrays, and a key that contains a dot can be quoted, as in a."b.c". Returns Null when the path does not resolve.

fn (Doc) value_opt #

fn (d Doc) value_opt(key string) !Any

value_opt queries a value and returns an error when the path does not resolve, instead of Null.

fn (Doc) get #

fn (d Doc) get(key string) ?Any

get queries a value and returns an option, so none distinguishes a missing key from an explicit null.

fn (Doc) decode #

fn (d Doc) decode[T]() !T

decode decodes the document into T. It is the method form of the package-level decode and shares its attribute and hook support.

fn (Doc) reflect #

fn (d Doc) reflect[T]() T

reflect sets the fields of T from the matching document keys, leaving absent fields at their default value. Fields are matched by name or by a @[json5: 'name'] attribute. Unlike decode, a key that cannot be decoded into its field is silently skipped instead of reported, so a partially matching document still fills in what it can.

struct EncodeOpts #

@[params]
struct EncodeOpts {
pub:
	indent          string // indent unit; the zero value writes compact output
	single_quotes   bool   // write strings with `'` instead of `"`
	unquoted_keys   bool   // write object keys bare when they are valid identifiers
	trailing_commas bool   // write a comma after the last array element and member
}

EncodeOpts controls how encode writes JSON5 text.

struct EnumError #

struct EnumError {
	Error
pub:
	enum_name string
	value     string
}

EnumError describes a string that does not name a variant of the target enum.

fn (EnumError) msg #

fn (e &EnumError) msg() string

msg formats an EnumError for IError.msg().

struct NameError #

struct NameError {
	Error
pub:
	key string
}

NameError describes a missing object key.

fn (NameError) msg #

fn (e &NameError) msg() string

msg formats a NameError for IError.msg().

struct Null #

struct Null {
}

Null is the JSON5 null literal.

fn (Null) str #

fn (n Null) str() string

str returns Null rendered as JSON5 text.

struct Number #

struct Number {
pub:
	text string // literal source text, for example `0x1F` or `-Infinity`
}

Number is a JSON5 number. The literal source text is preserved so that 0x1F, +1, .5, 5., Infinity and NaN can be written back out unchanged.

fn (Number) str #

fn (n Number) str() string

str returns the number as its original literal text.

fn (Number) i64 #

fn (n Number) i64() i64

i64 returns the number as an i64. Hexadecimal and decimal integers convert exactly; fractional and non-finite values truncate toward zero.

fn (Number) int #

fn (n Number) int() int

int returns the number as an int.

fn (Number) u64 #

fn (n Number) u64() u64

u64 returns the number as a u64. Negative values clamp to zero.

fn (Number) f64 #

fn (n Number) f64() f64

f64 returns the number as an f64. Infinity and NaN keep their IEEE value.

struct ParseError #

struct ParseError {
	Error
pub:
	message string
	line    int
	col     int
}

ParseError describes a lexical or syntactic problem in the input.

fn (ParseError) msg #

fn (e &ParseError) msg() string

msg formats a ParseError for IError.msg().

struct Parser #

struct Parser {
mut:
	scanner &Scanner
	tok     Token
	started bool
}

Parser turns a stream of JSON5 tokens into an Any tree.

fn (Parser) parse #

fn (mut p Parser) parse() !Any

parse reads a single JSON5 value and checks that nothing but trivia follows.

struct PathStep #

struct PathStep {
pub:
	key   string
	index int
}

PathStep is one element of a parsed lookup path: either an object key or an array index.

fn (PathStep) is_index #

fn (s PathStep) is_index() bool

is_index reports whether the step indexes an array.

fn (PathStep) str #

fn (s PathStep) str() string

str returns the step in path syntax.

struct Pos #

struct Pos {
pub:
	line int
	col  int
}

Pos is a location inside the scanned text.

struct Raw #

struct Raw {
pub:
	text string
}

Raw is JSON5 text that the encoder writes verbatim. A type's to_json5() method produces a Raw, which is how a custom encoder can control quoting, comments and spacing.

struct Scanner #

struct Scanner {
pub:
	text []rune
mut:
	pos  int
	line int = 1
	col  int = 1
}

Scanner tokenizes JSON5 text. Whitespace and comments are treated as trivia and skipped; every other token is reported to the caller.

fn (Scanner) next #

fn (mut s Scanner) next() !Token

next returns the next token, skipping whitespace and comments.

struct Token #

struct Token {
pub:
	kind TokenKind
	lit  string // decoded value for strings, raw source text otherwise
	pos  Pos
}

Token is a single lexical unit produced by the Scanner.

struct TypeError #

struct TypeError {
	Error
pub:
	message string
	path    string // dotted path of the offending value, empty at the root
}

TypeError describes a value that does not fit the requested V type.

fn (TypeError) msg #

fn (e &TypeError) msg() string

msg formats a TypeError for IError.msg().