jsonvx
is a highly configurable JSON
parser
, querier
, and formatter
for Go.
It supports both strict JSON
(as defined by ECMA-404) and up to a more relaxed variant such as JSON5. This makes it ideal for parsing user-generated data, configuration files, or legacy formats that don't fully comply with the JSON specification.
With a single ParserConfig
struct, jsonvx
gives you fine-grained control over how JSON
is parsed. You can enable or disable features like:
- Hexadecimal numbers
(0xFF)
NaN
andInfinity
as numeric values- Leading plus signs in numbers
(+42)
- Decimal edge cases like
.5
or5.
- Unquoted object keys
({key: "value"})
- Single-quoted strings
('text')
- Newlines inside strings
- Escape characters outside the standard set
- Trailing commas in arrays or objects
- Line comments
(// comment)
and Block comments(/* comment */)
- Extra whitespace in unusual places
- Enable
JSON5
After parsing, jsonvx
gives you a clean abstract syntax tree (AST)
that you can either traverse manually or query using the built-in API. Each node in the tree implements a common JSON interface
, so you can safely inspect
, transform
, or stringify
data as needed.
jsonvx
is designed for flexibility and correctness — not raw performance. It prioritizes clarity and configurability over speed, making it perfect for tools, linters, formatters, and config loaders where input may vary or include non-standard extensions.
If you need full control over how JSON
is interpreted and a structured way to work with the result, jsonvx
is for you.
To start using jsonvx
, install Go and run go get
:
$ go get -u github.com/bube054/jsonvx
Get up and running with jsonvx
in seconds. Just create a new Parser
, parse your JSON
, and access fields using a simple path query system.
package main
import (
"fmt"
"github.com/bube054/jsonvx"
)
func main() {
data := []byte(`{
"name": {"first": "Tom", "last": "Anderson"},
"age": 37,
"children": ["Sara", "Alex", "Jack"],
"fav.movie": "Deer Hunter",
"friends": [
{"first": "Dale", "last": "Murphy", "age": 44, "nets": ["ig", "fb", "tw"]},
{"first": "Roger", "last": "Craig", "age": 68, "nets": ["fb", "tw"]},
{"first": "Jane", "last": "Murphy", "age": 47, "nets": ["ig", "tw"]}
]
}`)
parser := jsonvx.NewParser(data, nil)
node, err := parser.Parse()
if err != nil {
panic(fmt.Sprintf("failed to parse JSON: %s", err))
}
rootObj, ok := jsonvx.AsObject(node)
if !ok {
panic("expected root node to be an object")
}
ageNode, err := rootObj.QueryPath("age")
if err != nil {
panic(fmt.Sprintf("failed to query 'age' field: %s", err))
}
ageNum, ok := jsonvx.AsNumber(ageNode)
if !ok {
panic("expected 'age' to be a number")
}
ageValue, err := ageNum.Value()
if err != nil {
panic(fmt.Sprintf("failed to convert 'age' to numeric value: %s", err))
}
fmt.Println(ageValue) // 37
}
Access deeply nested fields in your parsed JSON
array
or object
structure using the QueryPath
method, which accepts a variadic list of strings to represent the path segments.
Using the json
data above, we can query for specific values using the QueryPath
method:
// Get first name
strNode, _ := rootObj.QueryPath("name", "first") // => "Tom"
// get children array
arrayNode, _ := rootObj.QueryPath("children") // => ["Sara", "Alex", "Jack"]
// Get second child
strNode, _ = rootObj.QueryPath("children", "1") // => "Alex"
// Get favorite movie (with dot in key name)
strNode, _ = rootObj.QueryPath("fav.movie") // => "Deer Hunter"
// Get first friend object
objNode, _ := rootObj.QueryPath("friends", "0") // => {"first": "Dale", "last": "Murphy", "age": 44, "nets": ["ig", "fb", "tw"]}
// Get last name of first friend
strNode, _ = rootObj.QueryPath("friends", "0", "last") // => "Murphy"
// Get second social network of second friend
strNode, _ = rootObj.QueryPath("friends", "1", "nets", "1") // => "tw"
// Get age of third friend
numNode, _ := rootObj.QueryPath("friends", "2", "age") // => 47
You can configure the Parser
using the functional options pattern, allowing you to enable relaxed JSON features individually. By default, the parser is strict (all options disabled), matching the ECMA-404 specification. To allow non-standard or user-friendly formats (like JSON5), pass options when creating the config:
Allows extra whitespace characters that are not normally permitted by strict JSON
.
cfg := jsonvx.NewParserConfig(jsonvx.WithAllowExtraWS(true))
parser := jsonvx.NewParser([]byte("{\"key\":\v\"value\"}"), cfg) // valid Vertical Tab
parser := jsonvx.NewParser([]byte("{\"key\":\f\"value\"}"), cfg) // valid Form Feed
parser := jsonvx.NewParser([]byte("{\"key\":\u0085\"value\"}"), cfg) // valid Next Line
parser := jsonvx.NewParser([]byte("{\"key\":\u00A0\"value\"}"), cfg) // valid No-Break Space
Enables support for hexadecimal numeric literals (e.g., 0x1F
).
cfg := jsonvx.NewParserConfig(jsonvx.WithAllowHexNumbers(true))
parser := jsonvx.NewParser([]byte("0x1F"), cfg) // valid 31 in hex
Allows numbers like .5
or 5.
without requiring a digit before/after the decimal point.
cfg := jsonvx.NewParserConfig(jsonvx.WithAllowPointEdgeNumbers(true))
parser := jsonvx.NewParser([]byte(".5"), cfg) // valid pre decimal point number
parser := jsonvx.NewParser([]byte("5."), cfg) // valid post decimal point number
Enables the use of Infinity
, -Infinity
and +Infinity
as number values.
cfg := jsonvx.NewParserConfig(jsonvx.WithAllowInfinity(true))
parser := jsonvx.NewParser([]byte("Infinity"), cfg) // valid positive infinity
parser := jsonvx.NewParser([]byte("-Infinity"), cfg) // valid negative infinity
parser := jsonvx.NewParser([]byte("+Infinity"), cfg) // valid only if AllowLeadingPlus is enabled
Allows NaN
(Not-a-Number) as a numeric value.
cfg := jsonvx.NewParserConfig(jsonvx.WithAllowNaN(true))
parser := jsonvx.NewParser([]byte("NaN"), cfg) // valid NaN
parser := jsonvx.NewParser([]byte("-NaN"), cfg) // valid NaN
parser := jsonvx.NewParser([]byte("+NaN"), cfg) // valid NaN only if AllowLeadingPlus is enabled
Permits a leading '+' in numbers (e.g., +42
).
cfg := jsonvx.NewParserConfig(jsonvx.WithAllowLeadingPlus(true))
parser := jsonvx.NewParser([]byte("+99"), cfg) // valid positive number
Enables parsing of unquoted object keys (e.g., {foo: "bar"}
)
cfg := jsonvx.NewParserConfig(jsonvx.WithAllowUnquoted(true))
parser := jsonvx.NewParser([]byte(`{foo: "bar"}`), cfg) // valid only for unquoted keys and not for unquoted values
Allows strings to be enclosed in single quotes (' '
) in addition to double quotes.
cfg := jsonvx.NewParserConfig(jsonvx.WithAllowSingleQuotes(true))
parser := jsonvx.NewParser([]byte(`{'name': 'Tom'}`), cfg) // valid single-quoted string
Permits multiple new line characters (\n
) being escaped.
cfg := jsonvx.NewParserConfig(jsonvx.WithAllowNewlineInStrings(true))
parser := jsonvx.NewParser([]byte(`"hello \
world"`), cfg) // valid escaped new line
Enables support for escape sequences other than \\
, \/
, \b
, \n
, \f
, \r
, \t
and Unicode escapes (\uXXXX
).
cfg := jsonvx.NewParserConfig(jsonvx.WithAllowOtherEscapeChars(true))
parser := jsonvx.NewParser([]byte(`"hello\qworld"`), cfg) // valid other escape character
Permits a trailing comma in array literals (e.g., [1, 2, ]
).
cfg := jsonvx.NewParserConfig(jsonvx.WithAllowTrailingCommaArray(true))
parser := jsonvx.NewParser([]byte(`[1, 2, 3, ]`), cfg) // valid array with trailing comma
Permits a trailing comma in object literals (e.g., {"a": 1,}
).
cfg := jsonvx.NewParserConfig(jsonvx.WithAllowTrailingCommaObject(true))
parser := jsonvx.NewParser([]byte(`{"a": 1,}`), cfg) // valid object with trailing comma
Enables the use of single-line comments (// ...).
cfg := jsonvx.NewParserConfig(jsonvx.WithAllowLineComments(true))
parser := jsonvx.NewParser([]byte(`// comment
123`), cfg) // valid number after liner comment
Enables the use of block comments (/_ ... _/).
cfg := jsonvx.NewParserConfig(jsonvx.WithAllowBlockComments(true))
parser := jsonvx.NewParser([]byte(`/* comment */ 123`), cfg)
Enables the use of JSON5
syntax.
parser := jsonvx.NewParser([]byte(`
{
// comments
unquoted: 'and you can quote me on that',
singleQuotes: 'I can use "double quotes" here',
lineBreaks: "Look, Mom! \
No \\n's!",
hexadecimal: 0xdecaf,
leadingDecimalPoint: .8675309, andTrailing: 8675309.,
positiveSign: +1,
trailingComma: 'in objects', andIn: ['arrays',],
"backwardsCompatible": "with JSON",
}
`), jsonvx.JSON5Config())
Below are examples of how to parse
, traverse
and retrieve
values from the parsed JSON
input using the Parser
.
Objects
are stored as aslice
ofjsonvx.KeyValue
struct.- keys are stored as
[]byte
andsorted
lexicographically
forlog(n)
value retrieval. - values are stored as
jsonvx.JSON
- Each key-value pair is accessible using the
ForEach
method. - Mixed value types (e.g.,
numbers
,strings
,objects
) are obviously supported. - Duplicate keys are included, but the first key-value pair is gotten with the
QueryPath
method.
This example shows how to parse a JSON
object using jsonvx
, cast it to an jsonvx.Object
node, and iterate over key-value pairs using the built-in ForEach
method.
parser := jsonvx.NewParser([]byte(`{"name": "Alice", "age": 30}`), jsonvx.NewParserConfig())
// Parse the JSON input
node, err := parser.Parse()
if err != nil {
panic(fmt.Sprintf("failed to parse JSON: %s", err))
}
// Cast the parsed node to an ObjectNode
objNode, ok := jsonvx.AsObject(node)
if !ok {
panic("expected root node to be an object")
}
// Iterate over key-value pairs using ForEach
objNode.ForEach(func(key []byte, value jsonvx.JSON, object *jsonvx.Object) {
fmt.Printf("Key: %s, Value: %v\n", string(key), value)
})
- Items are stored as a
slice
ofjsonvx.JSON
- Each item is accessible using the
ForEach
method.
This example demonstrates how to parse a JSON
array
using jsonvx
, cast it to an jsonvx.Array
node, and iterate over its elements using the built-in ForEach
method.
parser := jsonvx.NewParser([]byte(`[1, 2, 3]`), jsonvx.NewParserConfig())
// Parse the JSON input
node, err := parser.Parse()
if err != nil {
panic(fmt.Sprintf("failed to parse JSON: %s", err))
}
// Cast the parsed node to an ArrayNode
arrNode, ok := jsonvx.AsArray(node)
if !ok {
panic("expected root node to be an array")
}
// Iterate over each item using ForEach
arrNode.ForEach(func(item jsonvx.JSON, index int, array *jsonvx.Array) {
fmt.Printf("Item %d: %v\n", index, item)
})
- JSON string values are parsed as
jsonvx.String
. Value()
returns the raw Go` string value.
This example demonstrates how to parse a JSON
string
using jsonvx
, cast it to an jsonvx.String
node, and get its string
value using the built-in Value
method.
parser := jsonvx.NewParser([]byte(`"Hello, World!"`), jsonvx.NewParserConfig())
// Parse the JSON input
node, err := parser.Parse()
if err != nil {
panic(fmt.Sprintf("failed to parse JSON: %s", err))
}
// Cast the parsed node to a StringNode
strNode, ok := jsonvx.AsString(node)
if !ok {
panic("expected root node to be a string")
}
// Extract the underlying Go string value
strValue, err := strNode.Value()
if err != nil {
panic(fmt.Sprintf("failed to extract string value: %s", err))
}
fmt.Println(strValue) // Output: Hello, World!
- JSON numbers are parsed as a
jsonvx.Number
. Value()
returns the raw Gofloat64
value.Integer
,Float
,SciNot
,Hex
,Infinity
and ironicallyNaN
number types are supported.
This example demonstrates how to parse a JSON
number
using jsonvx
, cast it to an jsonvx.Number
node, and get float64
its value using the built-in Value
method.
parser := jsonvx.NewParser([]byte("123456"), jsonvx.NewParserConfig())
// Parse the JSON input
node, err := parser.Parse()
if err != nil {
panic(fmt.Sprintf("failed to parse JSON: %s", err))
}
// Cast the parsed node to a NumberNode
numNode, ok := jsonvx.AsNumber(node)
if !ok {
panic("expected root node to be a number")
}
// Extract the underlying Go float64 value
numValue, err := numNode.Value()
if err != nil {
panic(fmt.Sprintf("failed to extract number value: %s", err))
}
fmt.Println(numValue) // Output: 123456
- JSON booleans are parsed as a
jsonvx.Boolean
. Value()
returns the raw Gobool
value.
This example demonstrates how to parse a JSON
boolean
using jsonvx
, cast it to an jsonvx.Boolean
node, and get bool
its value using the built-in Value
method.
parser := jsonvx.NewParser([]byte("true"), jsonvx.NewParserConfig())
// Parse the JSON input
node, err := parser.Parse()
if err != nil {
panic(fmt.Sprintf("failed to parse JSON: %s", err))
}
// Cast the parsed node to a BooleanNode
boolNode, ok := jsonvx.AsBoolean(node)
if !ok {
panic("expected root node to be a boolean")
}
// Extract the underlying Go value
trueValue, err := boolNode.Value()
if err != nil {
panic(fmt.Sprintf("failed to extract boolean value: %s", err))
}
fmt.Println(trueValue) // Output: true
- JSON
null
values are parsed as ajsonvx.Null
. Value()
returns the raw Gonil
value.
This example demonstrates how to parse a JSON
null
using jsonvx
, cast it to an jsonvx.Null
node, and get nil
its value using the built-in Value
method.
parser := jsonvx.NewParser([]byte("null"), jsonvx.NewParserConfig())
// Parse the JSON input
node, err := parser.Parse()
if err != nil {
panic("failed to parse JSON: %s", err)
}
// Cast the parsed node to a NullNode
nullNode, ok := jsonvx.AsNull(node)
if !ok {
panic("expected root node to be null")
}
// Extract the underlying Go value
nilValue, _ := nullNode.Value()
fmt.Println(nilValue) // Output: <nil>
Comments
are ignored during parsing.- A
JSON
input with only comments and no data will result in a parse error.
parser := jsonvx.NewParser([]byte("/* Block Comment */"), jsonvx.NewParserConfig(
jsonvx.WithAllowBlockComments(true), jsonvx.WithAllowLineComments(true),
))
// parse the JSON
node, err := parser.Parse()
if err != nil {
panic(fmt.Sprintf("failed to parse JSON: %s", err))
}
- bube054 - Attah Gbubemi David (author)
This project is licensed under the MIT. See the LICENSE file for details.