gjson's Array() hides missing, and Value() turns every integer into a float64
Get JSON values quickly - JSON parser for Go
At a glance
- What is it?
- gjson is a single-file Go package that reads values out of a JSON document with dot-notation paths, wildcards, queries and modifiers instead of unmarshalling into a struct. It is fast, it has two dependencies, and it makes a few decisions you will meet in production: Array() returns an empty slice for missing or null values and wraps a non-null scalar in a one-element slice, Value() hands back float64 for every JSON number, and the old bracket query syntax still parses but is scheduled for removal.
- Who is it for?
- gjson fits Go code that reads a known path out of a large or loosely shaped document, especially where unmarshalling into a struct would be wasteful. It needs care in code that must distinguish absent from zero, preserve large integer identifiers, or accept paths from untrusted input without revalidating them.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 14, 2026, and from our analysis. They are not legal advice.
Star history
- tidwall/gjson15,562 ★ · +6
| Repository | 2026-09-22 | 2026-11-14 |
|---|---|---|
| tidwall/gjson | 15.6k | 15.6k |
6 Stars in the last 53 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
A path is a dot string, and a key containing a dot has to be escaped
Retrieval is one call. Get takes the document and a path in dot syntax, and returns as soon as it finds the value:
value := gjson.Get(json, "name.last")
println(value.String())The path language is small and worth learning properly, because it is a language. A path is a series of keys joined by dots. A key may contain the wildcards * and ?. To reach an array element you use the index as the key. The # character gives you the length of an array or opens a child path. The working example in the README includes a key with a dot in it, queried by escaping it:
"name.last" >> "Anderson"
"age" >> 37
"children.#" >> 3
"children.1" >> "Alex"
"child*.2" >> "Jack"
"c?ildren.0" >> "Sara"
"fav\.movie" >> "Deer Hunter"
"friends.#.first" >> ["Dale","Roger","Jane"]
"friends.1.last" >> "Craig"Two consequences follow. Any key containing a dot, an asterisk or a question mark is addressable only if you escape it, which means path construction has to be aware of the key it is building for. And the wildcards mean a path is not always a single lookup: child*.2 is a pattern, so it can match a key you did not have in mind.
The practical guidance is that a path built by concatenating untrusted strings is not the same operation as a lookup, because the untrusted part can introduce a wildcard or change the structure of the query.
Array() normalizes missing, null and scalar values
The accessor list is long, and three entries are worth reading together:
result.Exists() bool
result.Value() interface{}
result.Int() int64
result.Uint() uint64
result.Float() float64
result.String() string
result.Bool() bool
result.Time() time.Time
result.Array() []gjson.Result
result.Map() map[string]gjson.Result
result.Get(path string) Result
result.ForEach(iterator func(key, value Result) bool)
result.Less(token Result, caseSensitive bool) boolArray() is the one to be careful with. The README states two behaviours: missing or null values return an empty slice, and a non-null scalar returns a one-element slice.
So three distinct situations come back as the same Go type. A missing key gives you an empty slice. A scalar string gives you a one-element slice. A real array gives you its elements. If your code loops over the result and you forgot the Exists() check, a missing field silently becomes an empty loop and a scalar becomes a single iteration, and neither looks like an error.
That is not a design flaw so much as a deliberate choice to return a useful shape, but it means Exists() is not optional in defensive code. Use Exists() to detect absence, inspect the result type for null, and check IsArray() before Array() when an array is required; a valid array can also have one element.
The bracket query syntax still works and is scheduled to be removed
Queries came first in one form and then changed, and the change is documented with a version attached. Arrays can be searched with a #(...) construct that returns the first match, and #(...)# for all matches:
friends.#(last=="Murphy").first >> "Dale"
friends.#(last=="Murphy")#.first >> ["Dale","Jane"]
friends.#(age>45)#.last >> ["Craig","Murphy"]
friends.#(first%"D*").last >> "Murphy"
friends.#(first!%"D*").last >> "Craig"
friends.#(nets.#(=="fb"))#.first >> ["Dale","Roger"]The operators are the full comparison set, ==, !=, <, <=, > and >=, plus % for like and !% for not like, and queries nest, as the last example shows.
The compatibility note is the part worth acting on. Before v1.3.0 queries used the brackets, and that was changed to the parenthesis form to avoid confusion with the multipath syntax added around the same time. The README states that the bracketed form continues to work for backwards compatibility, and that it will do so until the next major release.
That is a better deprecation notice than most projects write, because it says the syntax goes away at the next major release. If you have inherited code with bracket queries, it is not urgent and it is not optional forever, so replacing it now costs an afternoon and avoids a breaking change arriving inside a Go module upgrade you did not choose.
Value() returns float64 for every JSON number, so use Int() for identifiers
The untyped accessor exists and it has a specific cost. Value() returns an interface{} that requires a type assertion, and the mapping is:
boolean >> bool
number >> float64
string >> string
null >> nil
array >> []interface{}
object >> map[string]interface{}Every number becomes a float64. That is the same choice the standard library's decoder makes, and it has the same consequence: a JSON integer that does not survive a float64 round trip comes back silently wrong rather than erroring. Account identifiers, database keys and anything else you intend to compare for equality are the cases that matter.
The library gives you the way out, and it is documented as a feature rather than a workaround. Int() and Uint() are described as capable of reading all 64 bits:
result.Int() int64 // -9223372036854775808 to 9223372036854775807
result.Uint() uint64 // 0 to 18446744073709551615So the rule is short: reach for Value() when you want to inspect an arbitrary result of unknown shape, and reach for Int() or Uint() the moment the number has an identity. If you are writing a decode helper, take a type parameter or an accessor function from the caller rather than collapsing to interface{} in the middle.
result.Index is zero when the index is unknown
Two fields on Result are about position rather than value, and both have an ambiguity built in.
result.Type // can be String, Number, True, False, Null, or JSON
result.Str // holds the string
result.Num // holds the float64 number
result.Raw // holds the raw json
result.Index // index of raw value in original json, zero means index unknown
result.Indexes // indexes of all the elements that match on a path containing the '#' query character.Index is documented as the position of the raw value in the original document, with zero meaning the index is unknown. So offset zero and not-found are the same value, and a caller who checks Index to decide whether a result exists will believe the first element of a document is missing.
That is why Exists() exists as a separate method, and why Index should be used for what it is actually good at: pointing a human at the offending part of a very large document. Reporting a byte offset in an error message is a genuinely useful thing to do with it.
Indexes is the plural form and has a narrower job, returning the positions of every element matching a path that contains the # query character. That makes it the right call when you have a query result and need to map each match back to the source, rather than to identify the document.
Two dependencies, both from the same author, and no tags to install
The module file is short enough to read in full:
module github.com/tidwall/gjson
go 1.23
require (
github.com/tidwall/match v1.1.1
github.com/tidwall/pretty v1.2.0
)Both required modules belong to the same author. That is not a criticism. But it does mean gjson's supply chain includes its author's other modules, so a compromise affecting one of them reaches this one too.
Two dependencies is otherwise an unusually small surface for a JSON library, and it is a consequence of the design. The repository is a handful of files: gjson.go, gjson_test.go, SYNTAX.md, go.mod, go.sum, the README and the licence. The library is essentially one file.
The module requires Go 1.23, which is the real floor to check against your own build. And the repository has no GitHub releases. The install instruction in the README is go get -u github.com/tidwall/gjson, and the last push to the repository was on 2026-10-05.
For a library this stable that is usually fine, and Go modules record the commit you resolved to.
Modifiers make the path string part program
Since version 1.2 a path component can transform the data rather than look it up. A modifier is a path component that performs custom processing, and multiple paths are chained with the pipe character:
"children|@reverse" >> ["Jack","Alex","Sara"]
"children|@reverse|0" >> "Jack"The built-in list covers reversal, whitespace removal and pretty printing with @reverse, @ugly and @pretty, the current element and document validity with @this and @valid, array flattening with @flatten, object merging with @join, key and value enumeration with @keys and @values, and type conversion with @tostr and @fromstr, plus @group.
The capability is genuinely useful and it is compact. The consequence is that a path is no longer only a path. It is a small expression language that happens to be written as a string, which means three things worth noticing.
First, if paths come from a configuration file or a user, you are accepting modifier execution rather than a lookup, and @fromstr and @tostr will unwrap and wrap JSON strings, changing the type you get back without changing the path you wrote.
Second, the type conversions mean a path can quietly change shape, which interacts with the Array() behaviour from earlier in the most confusing way.
Third, a path that reads like configuration is a string you are now responsible for validating.
gjson reads; writing is a separate library from the same author
The README points outward three times, and each pointer tells you something about the scope.
SJSON is offered for modifying JSON. The JJ command line tool is also recommended. And the library has ports: one for Python, published by a different owner, and one for Rust, published by the same author.
So gjson itself is read-only. That is a coherent boundary and it is why the library is one file, but it means a workflow that reads a document and then writes it back needs two modules rather than one, and both are from the same author. If you are choosing between reading with gjson and writing with the standard library's encoder plus a struct, that is a real trade: gjson wins when the path is known and the shape is not, and loses when you are going to serialise the whole thing back out anyway.
The Python port being under a different owner is worth flagging if you are reading the same paths in both languages. Feature parity is not something a third-party port promises, and a path that works in the Go version is not automatically supported by a port aiming to track it.
One last detail: the README links a hosted playground, gjson-play, where you can try paths in a browser. Convenient for learning the syntax, and a place not to paste a document that contains anything you care about.
Editorial conclusion
gjson fits Go code that reads a known path out of a large or loosely shaped document, especially where unmarshalling into a struct would be wasteful. It needs care in code that must distinguish absent from zero, preserve large integer identifiers, or accept paths from untrusted input without revalidating them. Before adopting it, call Exists() and check IsArray() before Array() when an array is required, use Int() or Uint() rather than Value() for numeric identifiers, and replace any bracket-form queries now rather than at the next major release.
Frequently asked questions
How do I install gjson in a Go project?
Install Go and run go get -u github.com/tidwall/gjson. The module declares go 1.23 and depends on two modules, github.com/tidwall/match and github.com/tidwall/pretty, both published by the same author.
How do I read a nested value with gjson?
Pass a dot-separated path to gjson.Get, for example name.last, then read it with a typed accessor such as String(), Int(), Float() or Bool(). There is a GetBytes equivalent for working with a JSON byte slice, and the whole library is a single gjson.go file.
How do I query an array with gjson?
Use #(...) to return the first match and #(...)# to return all matches. Queries support the ==, !=, <, <=, > and >= operators plus % for like and !% for not like, and they nest, so a query such as friends.#(nets.#(=="fb"))#.first returns the first names of everyone with a facebook entry.
Is the old bracket query syntax still supported by gjson?
Yes, for backwards compatibility. Queries used the brackets before v1.3.0 and were changed to the parenthesis form to avoid confusion with multipath syntax, and the README states the bracketed form will keep working until the next major release.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/tidwall-gjson)Star history chart for your README
[](https://hysenlabs.com/projects/tidwall-gjson)Was this useful?