v2 / vlib / x / json2 / README.md
232 lines · 179 sloc · 4.95 KB · 9dec65ca6cd4c6699c58fad0457d9a21b93ead26
Raw

The name json2 was chosen to avoid any unwanted potential conflicts with the existing codegen tailored for the main json module which is powered by CJSON.

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

Usage

encode[T]

import x.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.

decode[T]

import x.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 x.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

x.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 x.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 x.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 x.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 x.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

x.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

x.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(, mut buffer)!

sb.write(buffer)!

unsafe { buffer.free() }