Skip to content

json2 #

json2 replaces the removed cJSON based json module. v fmt -w file.v
rewrites the usual json.decode(T, s), json.encode(x) and
json.encode_pretty(x) calls to json2, and leaves code it cannot rewrite
safely unchanged. By hand:

  • json.decode(T, s) becomes json2.decode[T](s)
  • json.encode(x) becomes json2.encode(x, escape_unicode: true, time_as_unix: true)
  • json.encode_pretty(x) becomes json2.encode(x, prettify: true, legacy_layout: true, escape_unicode: true, time_as_unix: true)

These options keep the output of the old module: non-ASCII characters escaped as
\uXXXX, time.Time values as Unix timestamps, and its tab based pretty layout.
One difference remains: a @[raw] field, or a string that receives a JSON object
or array, holds the JSON text exactly as written, while the old module returned it
without whitespace.

json.decode(?T, s) has no direct counterpart, since V does not accept ?T as a
type argument, and vfmt leaves such files unchanged: decode T with
json2.decode[T](s), and handle a null input yourself.

json.encode(x) wrote a sum type value narrowed by x is Cat or match x as the
whole sum type, with its _type field, while json2.encode(x) writes the narrowed
variant. vfmt leaves such files unchanged as well: cast the value back to its sum
type, as in json2.encode(Animal(x), escape_unicode: true, time_as_unix: true).

A type with its own to_json() method (or the deprecated json_str()), such as
big.Integer, is written by json2.encode through that method, while the old module
ignored it and wrote the type's fields. vfmt migrates these calls too, since it does
not resolve types, so check the output of such types after migrating. Decoding is not
affected: the from_json_* methods only handle a JSON string, number, boolean or
null, so objects written by the old module still decode field by field.

json2.decode also accepts an enum value given as the number of one of its members,
for an enum without @[json_as_number] too, as json2.encode(x, enum_as_int: true)
writes it; the old module only accepted the member's name there. Other numbers are
still rejected.

json2 is an experimental JSON parser written from scratch on V.

Usage

encode[T]

import json2
import time

struct Person {
mut:
    name     string
    age      ?int = 20
    birthday time.Time
    deathday ?time.Time
}

fn main() {
    mut person := Person{
        name:     'Bob'
        birthday: time.now()
    }
    person_json := json2.encode[Person](person)
    // person_json == {"name": "Bob", "age": 20, "birthday": "2022-03-11T13:54:25.000Z"}
}

Enums encode as strings by default. Use @[json_as_number] on an enum to emit its integer value instead.

Use @[omitempty] to omit empty struct fields. For boolean fields, including optional booleans, false is empty and true is encoded. @[omitempty] only affects encoding: decode still assigns an explicit empty value such as 0 or "" from the input.

decode[T]

JSON object keys are decoded to the target map key type, including signed and unsigned integer keys. Nested maps and maps stored in struct fields follow the same conversion.

The target type keeps its declaring module. A program's own sum type named Any is distinct from json2.Any, including through nested dynamic arrays, fixed arrays, and maps such as json2.decode[[][2]Any](text).

import json2
import time

struct Person {
mut:
    name     string
    age      ?int = 20
    birthday time.Time
    deathday ?time.Time
}

fn main() {
    resp := '{"name": "Bob", "age": 20, "birthday": "${time.now()}"}'
    person := json2.decode[Person](resp)!
    // struct Person {
    //    mut:
    //        name "Bob"
    //        age  20
    //        birthday "2022-03-11 13:54:25"
    //       deathday "2022-03-11 13:54:25"
    // }
}

decode[T] is smart and can auto-convert the types of struct fields - this means examples below will have the same result

Embedded struct fields are decoded from the surrounding object, including reference fields.

json2.decode[Person]('{"name": "Bob", "age": 20, "birthday": "2022-03-11T13:54:25.000Z"}')!
json2.decode[Person]('{"name": "Bob", "age": 20, "birthday": "2022-03-11 13:54:25.000"}')!
json2.decode[Person]('{"name": "Bob", "age": "20", "birthday": 1647006865}')!
json2.decode[Person]('{"name": "Bob", "age": "20", "birthday": "1647006865"}}')!

raw decode

import json2
import net.http

fn main() {
    resp := http.get('https://reqres.in/api/products/1')!

    // This returns an Any type
    raw_product := json2.decode[json2.Any](resp.body)!
}

iterative token scanning

json2 now exposes low-level scanners that let you process JSON token by token instead of materializing the whole tree first.

Use new_scanner() for in-memory strings:

import json2

fn main() {
    mut scanner := json2.new_scanner('{"items":[1,2,3]}')
    for {
        token := scanner.next()!
        if token.is_eof() {
            break
        }
        println('${token.kind}: ${token.literal()}')
    }
}

Use new_reader_scanner() to stream tokens from a file or any io.Reader:

import os
import json2

fn main() {
    mut file := os.open('huge.json')!
    defer {
        file.close()
    }

    mut scanner := json2.new_reader_scanner(reader: file)
    defer {
        scanner.free()
    }

    for {
        token := scanner.next()!
        if token.is_eof() {
            break
        }
        if token.kind == .str && token.literal() == 'id' {
            println('found an id key')
        }
    }
}

Casting Any type / Navigating

import json2
import net.http

fn main() {
    resp := http.get('https://reqres.in/api/products/1')!

    raw_product := json2.decode[json2.Any](resp.body)!

    product := raw_product.as_map()
    data := product['data'] as map[string]json2.Any

    id := data['id'].int() // 1
    name := data['name'].str() // cerulean
    year := data['year'].int() // 2000
}

Constructing an Any type

import json2

fn main() {
    mut me := map[string]json2.Any{}
    me['name'] = 'Bob'
    me['age'] = 18

    mut arr := []json2.Any{}
    arr << 'rock'
    arr << 'papers'
    arr << json2.null
    arr << 12

    me['interests'] = arr

    mut pets := map[string]json2.Any{}
    pets['Sam'] = 'Maltese Shitzu'
    me['pets'] = pets

    // Stringify to JSON
    println(me.str())
    //{
    //   "name":"Bob",
    //   "age":18,
    //   "interests":["rock","papers","scissors",null,12],
    //   "pets":{"Sam":"Maltese"}
    //}
}

Null Values

json2 has a separate Null type for differentiating an undefined value and a null value. To verify that the field you're accessing is a Null, use [typ] is json2.Null.

fn (mut p Person) from_json(f json2.Any) {
    obj := f.as_map()
    if obj['age'] is json2.Null {
        // use a default value
        p.age = 10
    }
}

Casting a value to an incompatible type

json2 provides methods for turning Any types into usable types. The following list shows the possible outputs when casting a value to an incompatible type.

  1. Casting non-array values with as_array() will return an array with the value as the content.
  2. Casting non-map values as map (as_map()) will return a map with the value as the content.
  3. Casting non-string values to string (str()) will return the JSON string representation of the value.
  4. Casting non-numeric values to int/float (int()/i64()/f32()/f64()) will return zero.

Encoding using string builder instead of []u8

To be more performant, json2, in PR 20052, decided to use buffers directly instead of Writers. If you want to use Writers you can follow the steps below:

mut sb := strings.new_builder(64)
mut buffer := []u8{}

json2.encode_value(<some value to be encoded here>, mut buffer)!

sb.write(buffer)!

unsafe { buffer.free() }

Constants #

const null = Null{}

null is an instance of the Null type, to ease comparisons with it.

fn decode #

@[manualfree]
fn decode[T](val string, params DecoderOptions) !T

decode decodes a JSON string into a specified type. By default, decoding is lenient. Use strict: true for strict JSON spec compliance.

fn encode #

fn encode[T](val T, config EncoderOptions) string

encode is a generic function that encodes a type into a JSON string.

fn encode_pretty #

@[deprecated: 'use `encode(..., prettify: true)` instead']
@[deprecated_after: '2025-10-30']
fn encode_pretty[T](typed_data T) string

encode_pretty ...

fn map_from #

fn map_from[T](t T) map[string]Any

map_from converts a struct to a map of Any.

fn new_reader_scanner #

fn new_reader_scanner(config ReaderScannerConfig) &ReaderScanner

new_reader_scanner creates an iterative scanner that reads JSON tokens from an io.Reader.

fn new_scanner #

fn new_scanner(text string) Scanner

new_scanner creates an iterative scanner for an in-memory JSON string.

fn new_scanner_from_bytes #

fn new_scanner_from_bytes(text []u8) Scanner

new_scanner_from_bytes creates an iterative scanner for an in-memory JSON byte slice.

fn raw_decode #

@[deprecated: 'use `decode[json2.Any]` instead']
@[deprecated_after: '2025-10-10']
fn raw_decode(src string) !Any

Decodes a JSON string into an Any type. Returns an option.

interface BooleanDecoder #

interface BooleanDecoder {
	// called with converted bool
	// already checked so no error needed
mut:
	from_json_boolean(boolean_value bool)
}

implements decoding json true/false

interface Encodable #

@[deprecated: 'use `to_json` to implement `JsonEncoder` instead']
@[deprecated_after: '2025-10-30']
interface Encodable {
	json_str() string
}

interface JsonEncoder #

interface JsonEncoder {
	// to_json returns a string containing an objects json representation
	to_json() string
}

implements encoding json, this is not validated so implementations must be correct

interface NullDecoder #

interface NullDecoder {
	// only has one value
	// already checked so no error needed
mut:
	from_json_null()
}

implements decoding json null

interface NumberDecoder #

interface NumberDecoder {
	// called with raw string of number e.g. '-1.234e23'
mut:
	from_json_number(raw_number string) !
}

implements decoding json numbers, e.g. -1.234e23

interface StringDecoder #

interface StringDecoder {
	// called with raw string (minus apostrophes) e.g. 'hello, \u2164!'
mut:
	from_json_string(raw_string string) !
}

implements decoding json strings, e.g. "hello, \u2164!"

fn (Any) str #

fn (f Any) str() string

str returns the string representation of the Any type. Use the json_str method. If you want to use the escaped str() version of the Any type.

fn (Any) json_str #

fn (f Any) json_str() string

json_str returns the JSON string representation of the Any type.

fn (Any) prettify_json_str #

@[deprecated: 'use `encode(Any(...), prettify: true)` instead']
@[deprecated_after: '2025-10-30']
fn (f Any) prettify_json_str() string

prettify_json_str returns the pretty-formatted JSON string representation of the Any type.

fn (Any) i8 #

fn (f Any) i8() i8

i8 uses Any as a 16-bit integer.

fn (Any) i16 #

fn (f Any) i16() i16

i16 uses Any as a 16-bit integer.

fn (Any) int #

fn (f Any) int() int

int uses Any as an integer.

fn (Any) i32 #

fn (f Any) i32() i32

i32 uses Any as a 32-bit integer.

fn (Any) i64 #

fn (f Any) i64() i64

i64 uses Any as a 64-bit integer.

fn (Any) u8 #

fn (f Any) u8() u8

u8 uses Any as a 8-bit unsigned integer.

fn (Any) u16 #

fn (f Any) u16() u16

u16 uses Any as a 16-bit unsigned integer.

fn (Any) u32 #

fn (f Any) u32() u32

u32 uses Any as a 32-bit unsigned integer.

fn (Any) u64 #

fn (f Any) u64() u64

u64 uses Any as a 64-bit unsigned integer.

fn (Any) f32 #

fn (f Any) f32() f32

f32 uses Any as a 32-bit float.

fn (Any) f64 #

fn (f Any) f64() f64

f64 uses Any as a 64-bit float.

fn (Any) bool #

fn (f Any) bool() bool

bool uses Any as a bool.

fn (Any) arr #

@[deprecated: 'use as_array() instead']
@[deprecated_after: '2026-03-27']
fn (f Any) arr() []Any

arr uses Any as an array.

fn (Any) as_array #

fn (f Any) as_array() []Any

as_array uses Any as an array.

fn (Any) as_map #

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

as_map uses Any as a map.

fn (Any) as_map_of_strings #

fn (f Any) as_map_of_strings() map[string]string

as_map_of_strings converts Any maps and arrays into maps of string values.

fn (Any) to_time #

fn (f Any) to_time() !time.Time

to_time uses Any as a time.Time.

fn ([]Any) str #

fn (f []Any) str() string

str returns the JSON string representation of the []Any type.

fn (map[string]Any) str #

fn (f map[string]Any) str() string

str returns the JSON string representation of the map[string]Any type.

enum TokenKind #

enum TokenKind {
	none
	error
	str
	float
	int
	null
	bool
	eof
	comma = 44 // ,
	colon = 58 // :
	lsbr  = 91 // [
	rsbr  = 93 // ]
	lcbr  = 123 // {
	rcbr  = 125 // }
}

TokenKind identifies the kind of a JSON token.

struct DecoderOptions #

@[markused; params]
struct DecoderOptions {
pub:
	// In strict mode, quoted strings are not accepted as numbers.
	// For example, '"123"' will fail to decode as int in strict mode,
	// but will succeed in default mode.
	// Strict mode also requires a fixed size array to get a JSON array with exactly
	// as many elements. By default, `null` or missing trailing elements keep their
	// default values, and extra elements are ignored.
	// Strict mode also rejects `null` for a value that is not an option. By default,
	// like in the removed `json` module, `null` is the zero value of a number, bool,
	// string, enum or time, an empty array or map, and a nested struct with its
	// default field values.
	strict bool
}

DecoderOptions provides options for JSON decoding. By default, decoding is lenient. Use strict: true for strict JSON spec compliance.

struct EncoderOptions #

@[params]
struct EncoderOptions {
pub:
	prettify       bool
	indent_string  string = '    '
	newline_string string = '\n'

	enum_as_int bool

	escape_unicode bool
	// time_as_unix encodes a `time.Time` as its Unix timestamp in seconds, like the
	// removed `json` module did, instead of an RFC 3339 string.
	time_as_unix bool
	// legacy_layout, with `prettify: true`, lays the output out like `encode_pretty`
	// of the removed `json` module: members indented with tabs, a tab after each
	// key, and array elements on one line, separated by `, `.
	legacy_layout bool
}

EncoderOptions provides a list of options for encoding

struct JsonDecodeError #

struct JsonDecodeError {
	Error
	context string
pub:
	message string

	line      int
	character int
}

struct JsonScanError #

struct JsonScanError {
	Error
pub:
	message string

	line      int
	character int
}

JsonScanError describes a tokenization error reported by the iterative scanner APIs.

struct Null #

struct Null {}

Null is a simple representation of the null value in JSON.

fn (Null) from_json_null #

@[markused]
fn (mut n Null) from_json_null()

from_json_null implements a custom decoder for json2

fn (Null) to_json #

fn (n Null) to_json() string

to_json implements a custom encoder for json2

struct ReaderScanner #

struct ReaderScanner {
mut:
	reader &io.BufferedReader
	peeked bool
	ch     u8
	line   int = 1
	col    int = 1
}

ReaderScanner tokenizes JSON incrementally from any io.Reader.

fn (ReaderScanner) free #

fn (mut s ReaderScanner) free()

free releases the reader scanner's internal buffer.

fn (ReaderScanner) next #

fn (mut s ReaderScanner) next() !Token

next returns the next JSON token from the reader-backed scanner.

struct ReaderScannerConfig #

@[params]
struct ReaderScannerConfig {
pub:
	reader      io.Reader
	buffer_size int = 128 * 1024
}

ReaderScannerConfig configures a reader-backed JSON scanner.

struct Scanner #

struct Scanner {
mut:
	text []u8
	pos  int // the position of the token in scanner text
	line int = 1
	col  int = 1
}

Scanner tokenizes JSON from an in-memory string or byte slice.

fn (Scanner) next #

fn (mut s Scanner) next() !Token

next returns the next JSON token from the in-memory scanner.

struct Token #

struct Token {
pub:
	lit  []u8      // literal representation of the token
	kind TokenKind // the token number/enum; for quick comparisons
	line int       // the line in the source where the token occurred
	col  int       // the column in the source where the token occurred
}

fn (Token) literal #

fn (t Token) literal() string

literal returns the token contents as a string.

fn (Token) full_col #

fn (t Token) full_col() int

full_col returns the full column information which includes the length.

fn (Token) is_eof #

fn (t Token) is_eof() bool

is_eof reports whether the token marks the end of the JSON stream.