# tidwall/sjson: setting JSON values in Go without decoding the document

> SJSON is a small Go package that edits a JSON document in place by path. It is fast because it never builds a map, and that same design is why malformed input and unusual paths behave differently than encoding/json.

**tidwall/sjson** — Set JSON values very quickly in Go

- Repository: https://github.com/tidwall/sjson
- Stars: 2,734 · Forks: 194
- Language: Go
- License: MIT
- Published: 2026-09-28 · Updated: 2026-09-28 · Language: en
- Canonical page: https://hysenlabs.com/projects/tidwall-sjson

## The problem SJSON solves: editing one field without decoding the whole document

Most Go code that changes a JSON document does it the obvious way: unmarshal into map[string]interface{} or a struct, change a field, marshal back. That round trip allocates a map, copies every string and number into Go values, and then serialises everything again, including the parts you never touched. The README's own benchmark table puts encoding/json into a map at 21236 ns/op with 150 allocations, against 805 ns/op and 3 allocations for SJSON on the same document.

The package targets a narrow case: you have a JSON document as a string or []byte, you know the path to the value you want to change, and you want the rest of the document preserved byte for byte. Configuration files, event payloads, and stored records that get one field updated are the natural fits. It is a Go library, not a service, so it is for people writing Go code, not for operators looking for a CLI. The README points anyone who wants a command line at a separate project, JJ.

## How the dot path and the in-place rewrite work

A path is a series of keys separated by dots. The README gives the example "name.last" against {"name":{"first":"Janet","last":"Prichard"},"age":47}, which returns the same document with the last name replaced. Array elements are addressed by numeric key, so "children.1" targets the second child, and "friends.1.last" reaches into an object inside an array.

Two path details matter in practice. The dot and colon characters can be escaped with a backslash, which is how you address a key that literally contains a dot, such as "fav.movie" in the README's sample document. A colon forces a numeric object key instead of an array index, so "users.:2313.name" edits the object keyed by the string "2313" rather than position 2313 in an array. The -1 key appends to an existing array, and writing past the end pads with nulls: setting "friends.4" on a two element array produces ["Andy","Carol",null,null,"Sara"].

The performance comes from not building a data structure. SJSON scans the raw bytes, finds the span of the value at the given path, and splices the new value in. The go.mod file lists only two dependencies, gjson and pretty, both from the same author, and the repository root holds a single implementation file, sjson.go, plus its test file. That is the whole surface area.

## Installing tidwall/sjson and setting your first value

The README's install step is a single go get. It requires Go to be installed first, and the go.mod declares go 1.14 as the language version.

```bash
go get -u github.com/tidwall/sjson
```

After that, Set takes the document, a dot path, and any value. The README's first example mutates a nested field and prints the result.

```go
package main

import "github.com/tidwall/sjson"

const json = `{"name":{"first":"Janet","last":"Prichard"},"age":47}`

func main() {
	value, _ := sjson.Set(json, "name.last", "Anderson")
	println(value)
}
```

The printed output is {"name":{"first":"Janet","last":"Anderson"},"age":47}. Note that Set returns the new document as a string and an error; the example discards the error, which is fine for a demo and wrong for production. Delete follows the same shape, taking the document and a path, and the README shows it removing both object fields and array elements, including the last element via "friends.-1".

Starting from an empty document works too. Calling Set with "" as the document and "name.last" as the path produces {"name":{"last":"Anderson"}}, so you can build a document incrementally rather than assembling a map first.

## Invalid JSON and malformed paths: what the README admits

The README is explicit that Set expects well-formed, validated JSON. Its wording is that invalid JSON will not panic, but it may return unexpected results. That is a real constraint, not a footnote. If your input comes from a network boundary or a file you do not control, you are responsible for validating it before it reaches SJSON, because the parser walks bytes and trusts the structure it finds.

Invalid paths may return an error, according to the README, which is a softer guarantee than it sounds. A path that fails partway through a splice can leave you with a returned value that is neither the original document nor the intended edit. The README does not document any rollback or atomicity guarantee, so the caller has to decide what to do with the returned string when the error is non-nil. Treating the result as unusable on error is the safe reading.

There is also a type boundary. SJSON handles nil, booleans, integers, floats, strings, []string and map[string]interface{} directly. For anything else it falls back to the encoding/json marshaller, which means the fast path stops applying and you inherit the allocation profile you were trying to avoid. And if your job is reading values rather than writing them, this is the wrong package: the README directs you to GJSON, a separate library.

## SJSON against encoding/json, Gabs and GJSON

The closest comparison is encoding/json itself. Decoding into a map or a struct gives you type safety, validation as a side effect of unmarshalling, and the ability to read and write many fields in one pass. SJSON gives up all of that in exchange for touching one path without disturbing the rest of the document. If you are changing five fields, five Set calls means five scans of the document, and the benchmark advantage narrows.

Gabs appears in the README's benchmark table at 21311 ns/op with 150 allocations, essentially the same profile as decoding into a map, because that is what it does: it wraps a decoded structure and gives you a friendlier API over it. The difference is not speed, it is whether the document ever becomes a Go value.

GJSON is the sibling library and the one the README names for retrieval. It reads values by the same dot path syntax, so the two share a mental model: GJSON for get, SJSON for set. If your workload is mostly reads with occasional writes, you will end up with both in your dependency list, and go.mod already pulls gjson in as a direct requirement of sjson.

## Maintenance, licence and the cost of upgrading

The repository is not archived, and the last push was on 2026-05-19. The dependency surface is two modules, gjson and pretty, both pinned in go.mod at v1.14.2 and v1.2.0 respectively. Because the package is a single file with no build tags, no code generation and no cgo, upgrading means changing a version in go.mod and running your tests; there is no migration step, no schema, and no runtime configuration to reconcile.

The licence is MIT, which permits use in closed source products provided the copyright notice and permission notice are retained. That is a permissive arrangement, but the repository's LICENSE file is the authoritative text and this article is not legal advice.

The practical upgrade risk is behavioural rather than structural. The README's promises about invalid JSON and invalid paths are loose, so a patch release that tightens parsing could change what your error handling sees. Pin the version and read the diff before bumping, because the test file, sjson_test.go, is the only specification of edge case behaviour that ships with the project.

## Conclusion

Adopt tidwall/sjson when you are patching one or a few fields in a JSON document you already hold as bytes and you want to avoid a full decode into a map or struct. Do not adopt it when you need to read many fields, validate a document, or merge two documents, since the README points at GJSON for retrieval and offers no merge operation. Before wiring it into a service, verify two things yourself: how your callers behave when Set returns an error alongside a partially written value, and whether your paths contain dots or colons that must be escaped with a backslash. The library itself is small enough to read: sjson.go and sjson_test.go are the whole implementation on master.

## FAQ

### What is tidwall/sjson used for?

It is a Go package that sets a value in a JSON document by dot path, such as "name.last", and also deletes values by path. The README describes it as a fast and simple way to set a value, and points to GJSON for reading values instead.

### How do I install tidwall/sjson?

Install Go first, then run go get -u github.com/tidwall/sjson, which retrieves the library. The go.mod file declares go 1.14 and depends on github.com/tidwall/gjson v1.14.2 and github.com/tidwall/pretty v1.2.0.

### How do I append to a JSON array with tidwall/sjson?

Use the -1 key in the path, so setting "children.-1" appends a new value to the end of the children array. Writing an index past the end instead pads the array with nulls, as the README shows with "friends.4" on a two element array.

### What happens if the JSON passed to tidwall/sjson is invalid?

The README states that Set expects well-formed, validated JSON, and that invalid JSON will not panic but may return unexpected results. It does not document any rollback, so validation is the caller's responsibility.

## Sources

- [Issues](https://github.com/tidwall/sjson/issues)
- [License: MIT](https://github.com/tidwall/sjson/blob/master/LICENSE)
- [README](https://github.com/tidwall/sjson/blob/master/README.md)
- [tidwall/sjson on GitHub](https://github.com/tidwall/sjson)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/tidwall-sjson
