├── .github └── workflows │ └── go.yml ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── logo.png ├── sjson.go └── sjson_test.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Set up Go 1.x 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: ^1.13 20 | 21 | - name: Check out code into the Go module directory 22 | uses: actions/checkout@v2 23 | 24 | - name: Get dependencies 25 | run: | 26 | go get -v -t -d ./... 27 | if [ -f Gopkg.toml ]; then 28 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 29 | dep ensure 30 | fi 31 | 32 | - name: Build 33 | run: go build -v . 34 | 35 | - name: Test 36 | run: go test -v . 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Josh Baker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | SJSON 5 |
6 | GoDoc 7 |

8 | 9 |

set a json value quickly

10 | 11 | SJSON is a Go package that provides a [very fast](#performance) and simple way to set a value in a json document. 12 | For quickly retrieving json values check out [GJSON](https://github.com/tidwall/gjson). 13 | 14 | For a command line interface check out [JJ](https://github.com/tidwall/jj). 15 | 16 | Getting Started 17 | =============== 18 | 19 | Installing 20 | ---------- 21 | 22 | To start using SJSON, install Go and run `go get`: 23 | 24 | ```sh 25 | $ go get -u github.com/tidwall/sjson 26 | ``` 27 | 28 | This will retrieve the library. 29 | 30 | Set a value 31 | ----------- 32 | Set sets the value for the specified path. 33 | A path is in dot syntax, such as "name.last" or "age". 34 | This function expects that the json is well-formed and validated. 35 | Invalid json will not panic, but it may return back unexpected results. 36 | Invalid paths may return an error. 37 | 38 | ```go 39 | package main 40 | 41 | import "github.com/tidwall/sjson" 42 | 43 | const json = `{"name":{"first":"Janet","last":"Prichard"},"age":47}` 44 | 45 | func main() { 46 | value, _ := sjson.Set(json, "name.last", "Anderson") 47 | println(value) 48 | } 49 | ``` 50 | 51 | This will print: 52 | 53 | ```json 54 | {"name":{"first":"Janet","last":"Anderson"},"age":47} 55 | ``` 56 | 57 | Path syntax 58 | ----------- 59 | 60 | A path is a series of keys separated by a dot. 61 | The dot and colon characters can be escaped with ``\``. 62 | 63 | ```json 64 | { 65 | "name": {"first": "Tom", "last": "Anderson"}, 66 | "age":37, 67 | "children": ["Sara","Alex","Jack"], 68 | "fav.movie": "Deer Hunter", 69 | "friends": [ 70 | {"first": "James", "last": "Murphy"}, 71 | {"first": "Roger", "last": "Craig"} 72 | ] 73 | } 74 | ``` 75 | ``` 76 | "name.last" >> "Anderson" 77 | "age" >> 37 78 | "children.1" >> "Alex" 79 | "friends.1.last" >> "Craig" 80 | ``` 81 | 82 | The `-1` key can be used to append a value to an existing array: 83 | 84 | ``` 85 | "children.-1" >> appends a new value to the end of the children array 86 | ``` 87 | 88 | Normally number keys are used to modify arrays, but it's possible to force a numeric object key by using the colon character: 89 | 90 | ```json 91 | { 92 | "users":{ 93 | "2313":{"name":"Sara"}, 94 | "7839":{"name":"Andy"} 95 | } 96 | } 97 | ``` 98 | 99 | A colon path would look like: 100 | 101 | ``` 102 | "users.:2313.name" >> "Sara" 103 | ``` 104 | 105 | Supported types 106 | --------------- 107 | 108 | Pretty much any type is supported: 109 | 110 | ```go 111 | sjson.Set(`{"key":true}`, "key", nil) 112 | sjson.Set(`{"key":true}`, "key", false) 113 | sjson.Set(`{"key":true}`, "key", 1) 114 | sjson.Set(`{"key":true}`, "key", 10.5) 115 | sjson.Set(`{"key":true}`, "key", "hello") 116 | sjson.Set(`{"key":true}`, "key", []string{"hello", "world"}) 117 | sjson.Set(`{"key":true}`, "key", map[string]interface{}{"hello":"world"}) 118 | ``` 119 | 120 | When a type is not recognized, SJSON will fallback to the `encoding/json` Marshaller. 121 | 122 | 123 | Examples 124 | -------- 125 | 126 | Set a value from empty document: 127 | ```go 128 | value, _ := sjson.Set("", "name", "Tom") 129 | println(value) 130 | 131 | // Output: 132 | // {"name":"Tom"} 133 | ``` 134 | 135 | Set a nested value from empty document: 136 | ```go 137 | value, _ := sjson.Set("", "name.last", "Anderson") 138 | println(value) 139 | 140 | // Output: 141 | // {"name":{"last":"Anderson"}} 142 | ``` 143 | 144 | Set a new value: 145 | ```go 146 | value, _ := sjson.Set(`{"name":{"last":"Anderson"}}`, "name.first", "Sara") 147 | println(value) 148 | 149 | // Output: 150 | // {"name":{"first":"Sara","last":"Anderson"}} 151 | ``` 152 | 153 | Update an existing value: 154 | ```go 155 | value, _ := sjson.Set(`{"name":{"last":"Anderson"}}`, "name.last", "Smith") 156 | println(value) 157 | 158 | // Output: 159 | // {"name":{"last":"Smith"}} 160 | ``` 161 | 162 | Set a new array value: 163 | ```go 164 | value, _ := sjson.Set(`{"friends":["Andy","Carol"]}`, "friends.2", "Sara") 165 | println(value) 166 | 167 | // Output: 168 | // {"friends":["Andy","Carol","Sara"] 169 | ``` 170 | 171 | Append an array value by using the `-1` key in a path: 172 | ```go 173 | value, _ := sjson.Set(`{"friends":["Andy","Carol"]}`, "friends.-1", "Sara") 174 | println(value) 175 | 176 | // Output: 177 | // {"friends":["Andy","Carol","Sara"] 178 | ``` 179 | 180 | Append an array value that is past the end: 181 | ```go 182 | value, _ := sjson.Set(`{"friends":["Andy","Carol"]}`, "friends.4", "Sara") 183 | println(value) 184 | 185 | // Output: 186 | // {"friends":["Andy","Carol",null,null,"Sara"] 187 | ``` 188 | 189 | Delete a value: 190 | ```go 191 | value, _ := sjson.Delete(`{"name":{"first":"Sara","last":"Anderson"}}`, "name.first") 192 | println(value) 193 | 194 | // Output: 195 | // {"name":{"last":"Anderson"}} 196 | ``` 197 | 198 | Delete an array value: 199 | ```go 200 | value, _ := sjson.Delete(`{"friends":["Andy","Carol"]}`, "friends.1") 201 | println(value) 202 | 203 | // Output: 204 | // {"friends":["Andy"]} 205 | ``` 206 | 207 | Delete the last array value: 208 | ```go 209 | value, _ := sjson.Delete(`{"friends":["Andy","Carol"]}`, "friends.-1") 210 | println(value) 211 | 212 | // Output: 213 | // {"friends":["Andy"]} 214 | ``` 215 | 216 | ## Performance 217 | 218 | Benchmarks of SJSON alongside [encoding/json](https://golang.org/pkg/encoding/json/), 219 | [ffjson](https://github.com/pquerna/ffjson), 220 | [EasyJSON](https://github.com/mailru/easyjson), 221 | and [Gabs](https://github.com/Jeffail/gabs) 222 | 223 | ``` 224 | Benchmark_SJSON-8 3000000 805 ns/op 1077 B/op 3 allocs/op 225 | Benchmark_SJSON_ReplaceInPlace-8 3000000 449 ns/op 0 B/op 0 allocs/op 226 | Benchmark_JSON_Map-8 300000 21236 ns/op 6392 B/op 150 allocs/op 227 | Benchmark_JSON_Struct-8 300000 14691 ns/op 1789 B/op 24 allocs/op 228 | Benchmark_Gabs-8 300000 21311 ns/op 6752 B/op 150 allocs/op 229 | Benchmark_FFJSON-8 300000 17673 ns/op 3589 B/op 47 allocs/op 230 | Benchmark_EasyJSON-8 1500000 3119 ns/op 1061 B/op 13 allocs/op 231 | ``` 232 | 233 | JSON document used: 234 | 235 | ```json 236 | { 237 | "widget": { 238 | "debug": "on", 239 | "window": { 240 | "title": "Sample Konfabulator Widget", 241 | "name": "main_window", 242 | "width": 500, 243 | "height": 500 244 | }, 245 | "image": { 246 | "src": "Images/Sun.png", 247 | "hOffset": 250, 248 | "vOffset": 250, 249 | "alignment": "center" 250 | }, 251 | "text": { 252 | "data": "Click Here", 253 | "size": 36, 254 | "style": "bold", 255 | "vOffset": 100, 256 | "alignment": "center", 257 | "onMouseUp": "sun1.opacity = (sun1.opacity / 100) * 90;" 258 | } 259 | } 260 | } 261 | ``` 262 | 263 | Each operation was rotated though one of the following search paths: 264 | 265 | ``` 266 | widget.window.name 267 | widget.image.hOffset 268 | widget.text.onMouseUp 269 | ``` 270 | 271 | *These benchmarks were run on a MacBook Pro 15" 2.8 GHz Intel Core i7 using Go 1.7 and can be be found [here](https://github.com/tidwall/sjson-benchmarks)*. 272 | 273 | ## Contact 274 | Josh Baker [@tidwall](http://twitter.com/tidwall) 275 | 276 | ## License 277 | 278 | SJSON source code is available under the MIT [License](/LICENSE). 279 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tidwall/sjson 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/tidwall/gjson v1.14.2 7 | github.com/tidwall/pretty v1.2.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/tidwall/gjson v1.14.2 h1:6BBkirS0rAHjumnjHF6qgy5d2YAJ1TLIaFE2lzfOLqo= 2 | github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 3 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 4 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 5 | github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= 6 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 7 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidwall/sjson/b279807a1bad9fa155988667d97a3d2fff4061b5/logo.png -------------------------------------------------------------------------------- /sjson.go: -------------------------------------------------------------------------------- 1 | // Package sjson provides setting json values. 2 | package sjson 3 | 4 | import ( 5 | jsongo "encoding/json" 6 | "sort" 7 | "strconv" 8 | "unsafe" 9 | 10 | "github.com/tidwall/gjson" 11 | ) 12 | 13 | type errorType struct { 14 | msg string 15 | } 16 | 17 | func (err *errorType) Error() string { 18 | return err.msg 19 | } 20 | 21 | // Options represents additional options for the Set and Delete functions. 22 | type Options struct { 23 | // Optimistic is a hint that the value likely exists which 24 | // allows for the sjson to perform a fast-track search and replace. 25 | Optimistic bool 26 | // ReplaceInPlace is a hint to replace the input json rather than 27 | // allocate a new json byte slice. When this field is specified 28 | // the input json will not longer be valid and it should not be used 29 | // In the case when the destination slice doesn't have enough free 30 | // bytes to replace the data in place, a new bytes slice will be 31 | // created under the hood. 32 | // The Optimistic flag must be set to true and the input must be a 33 | // byte slice in order to use this field. 34 | ReplaceInPlace bool 35 | } 36 | 37 | type pathResult struct { 38 | part string // current key part 39 | gpart string // gjson get part 40 | path string // remaining path 41 | force bool // force a string key 42 | more bool // there is more path to parse 43 | } 44 | 45 | func isSimpleChar(ch byte) bool { 46 | switch ch { 47 | case '|', '#', '@', '*', '?': 48 | return false 49 | default: 50 | return true 51 | } 52 | } 53 | 54 | func parsePath(path string) (res pathResult, simple bool) { 55 | var r pathResult 56 | if len(path) > 0 && path[0] == ':' { 57 | r.force = true 58 | path = path[1:] 59 | } 60 | for i := 0; i < len(path); i++ { 61 | if path[i] == '.' { 62 | r.part = path[:i] 63 | r.gpart = path[:i] 64 | r.path = path[i+1:] 65 | r.more = true 66 | return r, true 67 | } 68 | if !isSimpleChar(path[i]) { 69 | return r, false 70 | } 71 | if path[i] == '\\' { 72 | // go into escape mode. this is a slower path that 73 | // strips off the escape character from the part. 74 | epart := []byte(path[:i]) 75 | gpart := []byte(path[:i+1]) 76 | i++ 77 | if i < len(path) { 78 | epart = append(epart, path[i]) 79 | gpart = append(gpart, path[i]) 80 | i++ 81 | for ; i < len(path); i++ { 82 | if path[i] == '\\' { 83 | gpart = append(gpart, '\\') 84 | i++ 85 | if i < len(path) { 86 | epart = append(epart, path[i]) 87 | gpart = append(gpart, path[i]) 88 | } 89 | continue 90 | } else if path[i] == '.' { 91 | r.part = string(epart) 92 | r.gpart = string(gpart) 93 | r.path = path[i+1:] 94 | r.more = true 95 | return r, true 96 | } else if !isSimpleChar(path[i]) { 97 | return r, false 98 | } 99 | epart = append(epart, path[i]) 100 | gpart = append(gpart, path[i]) 101 | } 102 | } 103 | // append the last part 104 | r.part = string(epart) 105 | r.gpart = string(gpart) 106 | return r, true 107 | } 108 | } 109 | r.part = path 110 | r.gpart = path 111 | return r, true 112 | } 113 | 114 | func mustMarshalString(s string) bool { 115 | for i := 0; i < len(s); i++ { 116 | if s[i] < ' ' || s[i] > 0x7f || s[i] == '"' || s[i] == '\\' { 117 | return true 118 | } 119 | } 120 | return false 121 | } 122 | 123 | // appendStringify makes a json string and appends to buf. 124 | func appendStringify(buf []byte, s string) []byte { 125 | if mustMarshalString(s) { 126 | b, _ := jsongo.Marshal(s) 127 | return append(buf, b...) 128 | } 129 | buf = append(buf, '"') 130 | buf = append(buf, s...) 131 | buf = append(buf, '"') 132 | return buf 133 | } 134 | 135 | // appendBuild builds a json block from a json path. 136 | func appendBuild(buf []byte, array bool, paths []pathResult, raw string, 137 | stringify bool) []byte { 138 | if !array { 139 | buf = appendStringify(buf, paths[0].part) 140 | buf = append(buf, ':') 141 | } 142 | if len(paths) > 1 { 143 | n, numeric := atoui(paths[1]) 144 | if numeric || (!paths[1].force && paths[1].part == "-1") { 145 | buf = append(buf, '[') 146 | buf = appendRepeat(buf, "null,", n) 147 | buf = appendBuild(buf, true, paths[1:], raw, stringify) 148 | buf = append(buf, ']') 149 | } else { 150 | buf = append(buf, '{') 151 | buf = appendBuild(buf, false, paths[1:], raw, stringify) 152 | buf = append(buf, '}') 153 | } 154 | } else { 155 | if stringify { 156 | buf = appendStringify(buf, raw) 157 | } else { 158 | buf = append(buf, raw...) 159 | } 160 | } 161 | return buf 162 | } 163 | 164 | // atoui does a rip conversion of string -> unigned int. 165 | func atoui(r pathResult) (n int, ok bool) { 166 | if r.force { 167 | return 0, false 168 | } 169 | for i := 0; i < len(r.part); i++ { 170 | if r.part[i] < '0' || r.part[i] > '9' { 171 | return 0, false 172 | } 173 | n = n*10 + int(r.part[i]-'0') 174 | } 175 | return n, true 176 | } 177 | 178 | // appendRepeat repeats string "n" times and appends to buf. 179 | func appendRepeat(buf []byte, s string, n int) []byte { 180 | for i := 0; i < n; i++ { 181 | buf = append(buf, s...) 182 | } 183 | return buf 184 | } 185 | 186 | // trim does a rip trim 187 | func trim(s string) string { 188 | for len(s) > 0 { 189 | if s[0] <= ' ' { 190 | s = s[1:] 191 | continue 192 | } 193 | break 194 | } 195 | for len(s) > 0 { 196 | if s[len(s)-1] <= ' ' { 197 | s = s[:len(s)-1] 198 | continue 199 | } 200 | break 201 | } 202 | return s 203 | } 204 | 205 | // deleteTailItem deletes the previous key or comma. 206 | func deleteTailItem(buf []byte) ([]byte, bool) { 207 | loop: 208 | for i := len(buf) - 1; i >= 0; i-- { 209 | // look for either a ',',':','[' 210 | switch buf[i] { 211 | case '[': 212 | return buf, true 213 | case ',': 214 | return buf[:i], false 215 | case ':': 216 | // delete tail string 217 | i-- 218 | for ; i >= 0; i-- { 219 | if buf[i] == '"' { 220 | i-- 221 | for ; i >= 0; i-- { 222 | if buf[i] == '"' { 223 | i-- 224 | if i >= 0 && buf[i] == '\\' { 225 | i-- 226 | continue 227 | } 228 | for ; i >= 0; i-- { 229 | // look for either a ',','{' 230 | switch buf[i] { 231 | case '{': 232 | return buf[:i+1], true 233 | case ',': 234 | return buf[:i], false 235 | } 236 | } 237 | } 238 | } 239 | break 240 | } 241 | } 242 | break loop 243 | } 244 | } 245 | return buf, false 246 | } 247 | 248 | var errNoChange = &errorType{"no change"} 249 | 250 | func appendRawPaths(buf []byte, jstr string, paths []pathResult, raw string, 251 | stringify, del bool) ([]byte, error) { 252 | var err error 253 | var res gjson.Result 254 | var found bool 255 | if del { 256 | if paths[0].part == "-1" && !paths[0].force { 257 | res = gjson.Get(jstr, "#") 258 | if res.Int() > 0 { 259 | res = gjson.Get(jstr, strconv.FormatInt(int64(res.Int()-1), 10)) 260 | found = true 261 | } 262 | } 263 | } 264 | if !found { 265 | res = gjson.Get(jstr, paths[0].gpart) 266 | } 267 | if res.Index > 0 { 268 | if len(paths) > 1 { 269 | buf = append(buf, jstr[:res.Index]...) 270 | buf, err = appendRawPaths(buf, res.Raw, paths[1:], raw, 271 | stringify, del) 272 | if err != nil { 273 | return nil, err 274 | } 275 | buf = append(buf, jstr[res.Index+len(res.Raw):]...) 276 | return buf, nil 277 | } 278 | buf = append(buf, jstr[:res.Index]...) 279 | var exidx int // additional forward stripping 280 | if del { 281 | var delNextComma bool 282 | buf, delNextComma = deleteTailItem(buf) 283 | if delNextComma { 284 | i, j := res.Index+len(res.Raw), 0 285 | for ; i < len(jstr); i, j = i+1, j+1 { 286 | if jstr[i] <= ' ' { 287 | continue 288 | } 289 | if jstr[i] == ',' { 290 | exidx = j + 1 291 | } 292 | break 293 | } 294 | } 295 | } else { 296 | if stringify { 297 | buf = appendStringify(buf, raw) 298 | } else { 299 | buf = append(buf, raw...) 300 | } 301 | } 302 | buf = append(buf, jstr[res.Index+len(res.Raw)+exidx:]...) 303 | return buf, nil 304 | } 305 | if del { 306 | return nil, errNoChange 307 | } 308 | n, numeric := atoui(paths[0]) 309 | isempty := true 310 | for i := 0; i < len(jstr); i++ { 311 | if jstr[i] > ' ' { 312 | isempty = false 313 | break 314 | } 315 | } 316 | if isempty { 317 | if numeric { 318 | jstr = "[]" 319 | } else { 320 | jstr = "{}" 321 | } 322 | } 323 | jsres := gjson.Parse(jstr) 324 | if jsres.Type != gjson.JSON { 325 | if numeric { 326 | jstr = "[]" 327 | } else { 328 | jstr = "{}" 329 | } 330 | jsres = gjson.Parse(jstr) 331 | } 332 | var comma bool 333 | for i := 1; i < len(jsres.Raw); i++ { 334 | if jsres.Raw[i] <= ' ' { 335 | continue 336 | } 337 | if jsres.Raw[i] == '}' || jsres.Raw[i] == ']' { 338 | break 339 | } 340 | comma = true 341 | break 342 | } 343 | switch jsres.Raw[0] { 344 | default: 345 | return nil, &errorType{"json must be an object or array"} 346 | case '{': 347 | end := len(jsres.Raw) - 1 348 | for ; end > 0; end-- { 349 | if jsres.Raw[end] == '}' { 350 | break 351 | } 352 | } 353 | buf = append(buf, jsres.Raw[:end]...) 354 | if comma { 355 | buf = append(buf, ',') 356 | } 357 | buf = appendBuild(buf, false, paths, raw, stringify) 358 | buf = append(buf, '}') 359 | return buf, nil 360 | case '[': 361 | var appendit bool 362 | if !numeric { 363 | if paths[0].part == "-1" && !paths[0].force { 364 | appendit = true 365 | } else { 366 | return nil, &errorType{ 367 | "cannot set array element for non-numeric key '" + 368 | paths[0].part + "'"} 369 | } 370 | } 371 | if appendit { 372 | njson := trim(jsres.Raw) 373 | if njson[len(njson)-1] == ']' { 374 | njson = njson[:len(njson)-1] 375 | } 376 | buf = append(buf, njson...) 377 | if comma { 378 | buf = append(buf, ',') 379 | } 380 | 381 | buf = appendBuild(buf, true, paths, raw, stringify) 382 | buf = append(buf, ']') 383 | return buf, nil 384 | } 385 | buf = append(buf, '[') 386 | ress := jsres.Array() 387 | for i := 0; i < len(ress); i++ { 388 | if i > 0 { 389 | buf = append(buf, ',') 390 | } 391 | buf = append(buf, ress[i].Raw...) 392 | } 393 | if len(ress) == 0 { 394 | buf = appendRepeat(buf, "null,", n-len(ress)) 395 | } else { 396 | buf = appendRepeat(buf, ",null", n-len(ress)) 397 | if comma { 398 | buf = append(buf, ',') 399 | } 400 | } 401 | buf = appendBuild(buf, true, paths, raw, stringify) 402 | buf = append(buf, ']') 403 | return buf, nil 404 | } 405 | } 406 | 407 | func isOptimisticPath(path string) bool { 408 | for i := 0; i < len(path); i++ { 409 | if path[i] < '.' || path[i] > 'z' { 410 | return false 411 | } 412 | if path[i] > '9' && path[i] < 'A' { 413 | return false 414 | } 415 | if path[i] > 'z' { 416 | return false 417 | } 418 | } 419 | return true 420 | } 421 | 422 | // Set sets a json value for the specified path. 423 | // A path is in dot syntax, such as "name.last" or "age". 424 | // This function expects that the json is well-formed, and does not validate. 425 | // Invalid json will not panic, but it may return back unexpected results. 426 | // An error is returned if the path is not valid. 427 | // 428 | // A path is a series of keys separated by a dot. 429 | // 430 | // { 431 | // "name": {"first": "Tom", "last": "Anderson"}, 432 | // "age":37, 433 | // "children": ["Sara","Alex","Jack"], 434 | // "friends": [ 435 | // {"first": "James", "last": "Murphy"}, 436 | // {"first": "Roger", "last": "Craig"} 437 | // ] 438 | // } 439 | // "name.last" >> "Anderson" 440 | // "age" >> 37 441 | // "children.1" >> "Alex" 442 | // 443 | func Set(json, path string, value interface{}) (string, error) { 444 | return SetOptions(json, path, value, nil) 445 | } 446 | 447 | // SetBytes sets a json value for the specified path. 448 | // If working with bytes, this method preferred over 449 | // Set(string(data), path, value) 450 | func SetBytes(json []byte, path string, value interface{}) ([]byte, error) { 451 | return SetBytesOptions(json, path, value, nil) 452 | } 453 | 454 | // SetRaw sets a raw json value for the specified path. 455 | // This function works the same as Set except that the value is set as a 456 | // raw block of json. This allows for setting premarshalled json objects. 457 | func SetRaw(json, path, value string) (string, error) { 458 | return SetRawOptions(json, path, value, nil) 459 | } 460 | 461 | // SetRawOptions sets a raw json value for the specified path with options. 462 | // This furnction works the same as SetOptions except that the value is set 463 | // as a raw block of json. This allows for setting premarshalled json objects. 464 | func SetRawOptions(json, path, value string, opts *Options) (string, error) { 465 | var optimistic bool 466 | if opts != nil { 467 | optimistic = opts.Optimistic 468 | } 469 | res, err := set(json, path, value, false, false, optimistic, false) 470 | if err == errNoChange { 471 | return json, nil 472 | } 473 | return string(res), err 474 | } 475 | 476 | // SetRawBytes sets a raw json value for the specified path. 477 | // If working with bytes, this method preferred over 478 | // SetRaw(string(data), path, value) 479 | func SetRawBytes(json []byte, path string, value []byte) ([]byte, error) { 480 | return SetRawBytesOptions(json, path, value, nil) 481 | } 482 | 483 | type dtype struct{} 484 | 485 | // Delete deletes a value from json for the specified path. 486 | func Delete(json, path string) (string, error) { 487 | return Set(json, path, dtype{}) 488 | } 489 | 490 | // DeleteBytes deletes a value from json for the specified path. 491 | func DeleteBytes(json []byte, path string) ([]byte, error) { 492 | return SetBytes(json, path, dtype{}) 493 | } 494 | 495 | type stringHeader struct { 496 | data unsafe.Pointer 497 | len int 498 | } 499 | 500 | type sliceHeader struct { 501 | data unsafe.Pointer 502 | len int 503 | cap int 504 | } 505 | 506 | func set(jstr, path, raw string, 507 | stringify, del, optimistic, inplace bool) ([]byte, error) { 508 | if path == "" { 509 | return []byte(jstr), &errorType{"path cannot be empty"} 510 | } 511 | if !del && optimistic && isOptimisticPath(path) { 512 | res := gjson.Get(jstr, path) 513 | if res.Exists() && res.Index > 0 { 514 | sz := len(jstr) - len(res.Raw) + len(raw) 515 | if stringify { 516 | sz += 2 517 | } 518 | if inplace && sz <= len(jstr) { 519 | if !stringify || !mustMarshalString(raw) { 520 | jsonh := *(*stringHeader)(unsafe.Pointer(&jstr)) 521 | jsonbh := sliceHeader{ 522 | data: jsonh.data, len: jsonh.len, cap: jsonh.len} 523 | jbytes := *(*[]byte)(unsafe.Pointer(&jsonbh)) 524 | if stringify { 525 | jbytes[res.Index] = '"' 526 | copy(jbytes[res.Index+1:], []byte(raw)) 527 | jbytes[res.Index+1+len(raw)] = '"' 528 | copy(jbytes[res.Index+1+len(raw)+1:], 529 | jbytes[res.Index+len(res.Raw):]) 530 | } else { 531 | copy(jbytes[res.Index:], []byte(raw)) 532 | copy(jbytes[res.Index+len(raw):], 533 | jbytes[res.Index+len(res.Raw):]) 534 | } 535 | return jbytes[:sz], nil 536 | } 537 | return []byte(jstr), nil 538 | } 539 | buf := make([]byte, 0, sz) 540 | buf = append(buf, jstr[:res.Index]...) 541 | if stringify { 542 | buf = appendStringify(buf, raw) 543 | } else { 544 | buf = append(buf, raw...) 545 | } 546 | buf = append(buf, jstr[res.Index+len(res.Raw):]...) 547 | return buf, nil 548 | } 549 | } 550 | var paths []pathResult 551 | r, simple := parsePath(path) 552 | if simple { 553 | paths = append(paths, r) 554 | for r.more { 555 | r, simple = parsePath(r.path) 556 | if !simple { 557 | break 558 | } 559 | paths = append(paths, r) 560 | } 561 | } 562 | if !simple { 563 | if del { 564 | return []byte(jstr), 565 | &errorType{"cannot delete value from a complex path"} 566 | } 567 | return setComplexPath(jstr, path, raw, stringify) 568 | } 569 | njson, err := appendRawPaths(nil, jstr, paths, raw, stringify, del) 570 | if err != nil { 571 | return []byte(jstr), err 572 | } 573 | return njson, nil 574 | } 575 | 576 | func setComplexPath(jstr, path, raw string, stringify bool) ([]byte, error) { 577 | res := gjson.Get(jstr, path) 578 | if !res.Exists() || !(res.Index != 0 || len(res.Indexes) != 0) { 579 | return []byte(jstr), errNoChange 580 | } 581 | if res.Index != 0 { 582 | njson := []byte(jstr[:res.Index]) 583 | if stringify { 584 | njson = appendStringify(njson, raw) 585 | } else { 586 | njson = append(njson, raw...) 587 | } 588 | njson = append(njson, jstr[res.Index+len(res.Raw):]...) 589 | jstr = string(njson) 590 | } 591 | if len(res.Indexes) > 0 { 592 | type val struct { 593 | index int 594 | res gjson.Result 595 | } 596 | vals := make([]val, 0, len(res.Indexes)) 597 | res.ForEach(func(_, vres gjson.Result) bool { 598 | vals = append(vals, val{res: vres}) 599 | return true 600 | }) 601 | if len(res.Indexes) != len(vals) { 602 | return []byte(jstr), errNoChange 603 | } 604 | for i := 0; i < len(res.Indexes); i++ { 605 | vals[i].index = res.Indexes[i] 606 | } 607 | sort.SliceStable(vals, func(i, j int) bool { 608 | return vals[i].index > vals[j].index 609 | }) 610 | for _, val := range vals { 611 | vres := val.res 612 | index := val.index 613 | njson := []byte(jstr[:index]) 614 | if stringify { 615 | njson = appendStringify(njson, raw) 616 | } else { 617 | njson = append(njson, raw...) 618 | } 619 | njson = append(njson, jstr[index+len(vres.Raw):]...) 620 | jstr = string(njson) 621 | } 622 | } 623 | return []byte(jstr), nil 624 | } 625 | 626 | // SetOptions sets a json value for the specified path with options. 627 | // A path is in dot syntax, such as "name.last" or "age". 628 | // This function expects that the json is well-formed, and does not validate. 629 | // Invalid json will not panic, but it may return back unexpected results. 630 | // An error is returned if the path is not valid. 631 | func SetOptions(json, path string, value interface{}, 632 | opts *Options) (string, error) { 633 | if opts != nil { 634 | if opts.ReplaceInPlace { 635 | // it's not safe to replace bytes in-place for strings 636 | // copy the Options and set options.ReplaceInPlace to false. 637 | nopts := *opts 638 | opts = &nopts 639 | opts.ReplaceInPlace = false 640 | } 641 | } 642 | jsonh := *(*stringHeader)(unsafe.Pointer(&json)) 643 | jsonbh := sliceHeader{data: jsonh.data, len: jsonh.len, cap: jsonh.len} 644 | jsonb := *(*[]byte)(unsafe.Pointer(&jsonbh)) 645 | res, err := SetBytesOptions(jsonb, path, value, opts) 646 | return string(res), err 647 | } 648 | 649 | // SetBytesOptions sets a json value for the specified path with options. 650 | // If working with bytes, this method preferred over 651 | // SetOptions(string(data), path, value) 652 | func SetBytesOptions(json []byte, path string, value interface{}, 653 | opts *Options) ([]byte, error) { 654 | var optimistic, inplace bool 655 | if opts != nil { 656 | optimistic = opts.Optimistic 657 | inplace = opts.ReplaceInPlace 658 | } 659 | jstr := *(*string)(unsafe.Pointer(&json)) 660 | var res []byte 661 | var err error 662 | switch v := value.(type) { 663 | default: 664 | b, merr := jsongo.Marshal(value) 665 | if merr != nil { 666 | return nil, merr 667 | } 668 | raw := *(*string)(unsafe.Pointer(&b)) 669 | res, err = set(jstr, path, raw, false, false, optimistic, inplace) 670 | case dtype: 671 | res, err = set(jstr, path, "", false, true, optimistic, inplace) 672 | case string: 673 | res, err = set(jstr, path, v, true, false, optimistic, inplace) 674 | case []byte: 675 | raw := *(*string)(unsafe.Pointer(&v)) 676 | res, err = set(jstr, path, raw, true, false, optimistic, inplace) 677 | case bool: 678 | if v { 679 | res, err = set(jstr, path, "true", false, false, optimistic, inplace) 680 | } else { 681 | res, err = set(jstr, path, "false", false, false, optimistic, inplace) 682 | } 683 | case int8: 684 | res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), 685 | false, false, optimistic, inplace) 686 | case int16: 687 | res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), 688 | false, false, optimistic, inplace) 689 | case int32: 690 | res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), 691 | false, false, optimistic, inplace) 692 | case int64: 693 | res, err = set(jstr, path, strconv.FormatInt(int64(v), 10), 694 | false, false, optimistic, inplace) 695 | case uint8: 696 | res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), 697 | false, false, optimistic, inplace) 698 | case uint16: 699 | res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), 700 | false, false, optimistic, inplace) 701 | case uint32: 702 | res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), 703 | false, false, optimistic, inplace) 704 | case uint64: 705 | res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10), 706 | false, false, optimistic, inplace) 707 | case float32: 708 | res, err = set(jstr, path, strconv.FormatFloat(float64(v), 'f', -1, 64), 709 | false, false, optimistic, inplace) 710 | case float64: 711 | res, err = set(jstr, path, strconv.FormatFloat(float64(v), 'f', -1, 64), 712 | false, false, optimistic, inplace) 713 | } 714 | if err == errNoChange { 715 | return json, nil 716 | } 717 | return res, err 718 | } 719 | 720 | // SetRawBytesOptions sets a raw json value for the specified path with options. 721 | // If working with bytes, this method preferred over 722 | // SetRawOptions(string(data), path, value, opts) 723 | func SetRawBytesOptions(json []byte, path string, value []byte, 724 | opts *Options) ([]byte, error) { 725 | jstr := *(*string)(unsafe.Pointer(&json)) 726 | vstr := *(*string)(unsafe.Pointer(&value)) 727 | var optimistic, inplace bool 728 | if opts != nil { 729 | optimistic = opts.Optimistic 730 | inplace = opts.ReplaceInPlace 731 | } 732 | res, err := set(jstr, path, vstr, false, false, optimistic, inplace) 733 | if err == errNoChange { 734 | return json, nil 735 | } 736 | return res, err 737 | } 738 | -------------------------------------------------------------------------------- /sjson_test.go: -------------------------------------------------------------------------------- 1 | package sjson 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "math/rand" 7 | "testing" 8 | "time" 9 | 10 | "github.com/tidwall/gjson" 11 | "github.com/tidwall/pretty" 12 | ) 13 | 14 | const ( 15 | setRaw = 1 16 | setBool = 2 17 | setInt = 3 18 | setFloat = 4 19 | setString = 5 20 | setDelete = 6 21 | ) 22 | 23 | func sortJSON(json string) string { 24 | opts := pretty.Options{SortKeys: true} 25 | return string(pretty.Ugly(pretty.PrettyOptions([]byte(json), &opts))) 26 | } 27 | 28 | func testRaw(t *testing.T, kind int, expect, json, path string, value interface{}) { 29 | t.Helper() 30 | expect = sortJSON(expect) 31 | var json2 string 32 | var err error 33 | switch kind { 34 | default: 35 | json2, err = Set(json, path, value) 36 | case setRaw: 37 | json2, err = SetRaw(json, path, value.(string)) 38 | case setDelete: 39 | json2, err = Delete(json, path) 40 | } 41 | 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | json2 = sortJSON(json2) 46 | if json2 != expect { 47 | t.Fatalf("expected '%v', got '%v'", expect, json2) 48 | } 49 | var json3 []byte 50 | switch kind { 51 | default: 52 | json3, err = SetBytes([]byte(json), path, value) 53 | case setRaw: 54 | json3, err = SetRawBytes([]byte(json), path, []byte(value.(string))) 55 | case setDelete: 56 | json3, err = DeleteBytes([]byte(json), path) 57 | } 58 | json3 = []byte(sortJSON(string(json3))) 59 | if err != nil { 60 | t.Fatal(err) 61 | } else if string(json3) != expect { 62 | t.Fatalf("expected '%v', got '%v'", expect, string(json3)) 63 | } 64 | } 65 | func TestBasic(t *testing.T) { 66 | testRaw(t, setRaw, `[{"hiw":"planet","hi":"world"}]`, `[{"hi":"world"}]`, "0.hiw", `"planet"`) 67 | testRaw(t, setRaw, `[true]`, ``, "0", `true`) 68 | testRaw(t, setRaw, `[null,true]`, ``, "1", `true`) 69 | testRaw(t, setRaw, `[1,null,true]`, `[1]`, "2", `true`) 70 | testRaw(t, setRaw, `[1,true,false]`, `[1,null,false]`, "1", `true`) 71 | testRaw(t, setRaw, 72 | `[1,{"hello":"when","this":[0,null,2]},false]`, 73 | `[1,{"hello":"when","this":[0,1,2]},false]`, 74 | "1.this.1", `null`) 75 | testRaw(t, setRaw, 76 | `{"a":1,"b":{"hello":"when","this":[0,null,2]},"c":false}`, 77 | `{"a":1,"b":{"hello":"when","this":[0,1,2]},"c":false}`, 78 | "b.this.1", `null`) 79 | testRaw(t, setRaw, 80 | `{"a":1,"b":{"hello":"when","this":[0,null,2,null,4]},"c":false}`, 81 | `{"a":1,"b":{"hello":"when","this":[0,null,2]},"c":false}`, 82 | "b.this.4", `4`) 83 | testRaw(t, setRaw, 84 | `{"b":{"this":[null,null,null,null,4]}}`, 85 | ``, 86 | "b.this.4", `4`) 87 | testRaw(t, setRaw, 88 | `[null,{"this":[null,null,null,null,4]}]`, 89 | ``, 90 | "1.this.4", `4`) 91 | testRaw(t, setRaw, 92 | `{"1":{"this":[null,null,null,null,4]}}`, 93 | ``, 94 | ":1.this.4", `4`) 95 | testRaw(t, setRaw, 96 | `{":1":{"this":[null,null,null,null,4]}}`, 97 | ``, 98 | "\\:1.this.4", `4`) 99 | testRaw(t, setRaw, 100 | `{":\\1":{"this":[null,null,null,null,{".HI":4}]}}`, 101 | ``, 102 | "\\:\\\\1.this.4.\\.HI", `4`) 103 | testRaw(t, setRaw, 104 | `{"app.token":"cde"}`, 105 | `{"app.token":"abc"}`, 106 | "app\\.token", `"cde"`) 107 | testRaw(t, setRaw, 108 | `{"b":{"this":{"😇":""}}}`, 109 | ``, 110 | "b.this.😇", `""`) 111 | testRaw(t, setRaw, 112 | `[ 1,2 ,3]`, 113 | ` [ 1,2 ] `, 114 | "-1", `3`) 115 | testRaw(t, setInt, `[1234]`, ``, `0`, int64(1234)) 116 | testRaw(t, setFloat, `[1234.5]`, ``, `0`, float64(1234.5)) 117 | testRaw(t, setString, `["1234.5"]`, ``, `0`, "1234.5") 118 | testRaw(t, setBool, `[true]`, ``, `0`, true) 119 | testRaw(t, setBool, `[null]`, ``, `0`, nil) 120 | testRaw(t, setString, `{"arr":[1]}`, ``, `arr.-1`, 1) 121 | testRaw(t, setString, `{"a":"\\"}`, ``, `a`, "\\") 122 | testRaw(t, setString, `{"a":"C:\\Windows\\System32"}`, ``, `a`, `C:\Windows\System32`) 123 | } 124 | 125 | func TestDelete(t *testing.T) { 126 | testRaw(t, setDelete, `[456]`, `[123,456]`, `0`, nil) 127 | testRaw(t, setDelete, `[123,789]`, `[123,456,789]`, `1`, nil) 128 | testRaw(t, setDelete, `[123,456]`, `[123,456,789]`, `-1`, nil) 129 | testRaw(t, setDelete, `{"a":[123,456]}`, `{"a":[123,456,789]}`, `a.-1`, nil) 130 | testRaw(t, setDelete, `{"and":"another"}`, `{"this":"that","and":"another"}`, `this`, nil) 131 | testRaw(t, setDelete, `{"this":"that"}`, `{"this":"that","and":"another"}`, `and`, nil) 132 | testRaw(t, setDelete, `{}`, `{"and":"another"}`, `and`, nil) 133 | testRaw(t, setDelete, `{"1":"2"}`, `{"1":"2"}`, `3`, nil) 134 | } 135 | 136 | // TestRandomData is a fuzzing test that throws random data at SetRaw 137 | // function looking for panics. 138 | func TestRandomData(t *testing.T) { 139 | var lstr string 140 | defer func() { 141 | if v := recover(); v != nil { 142 | println("'" + hex.EncodeToString([]byte(lstr)) + "'") 143 | println("'" + lstr + "'") 144 | panic(v) 145 | } 146 | }() 147 | rand.Seed(time.Now().UnixNano()) 148 | b := make([]byte, 200) 149 | for i := 0; i < 2000000; i++ { 150 | n, err := rand.Read(b[:rand.Int()%len(b)]) 151 | if err != nil { 152 | t.Fatal(err) 153 | } 154 | lstr = string(b[:n]) 155 | SetRaw(lstr, "zzzz.zzzz.zzzz", "123") 156 | } 157 | } 158 | 159 | func TestDeleteIssue21(t *testing.T) { 160 | json := `{"country_code_from":"NZ","country_code_to":"SA","date_created":"2018-09-13T02:56:11.25783Z","date_updated":"2018-09-14T03:15:16.67356Z","disabled":false,"last_edited_by":"Developers","id":"a3e...bc454","merchant_id":"f2b...b91abf","signed_date":"2018-02-01T00:00:00Z","start_date":"2018-03-01T00:00:00Z","url":"https://www.google.com"}` 161 | res1 := gjson.Get(json, "date_updated") 162 | var err error 163 | json, err = Delete(json, "date_updated") 164 | if err != nil { 165 | t.Fatal(err) 166 | } 167 | res2 := gjson.Get(json, "date_updated") 168 | res3 := gjson.Get(json, "date_created") 169 | if !res1.Exists() || res2.Exists() || !res3.Exists() { 170 | t.Fatal("bad news") 171 | } 172 | 173 | // We change the number of characters in this to make the section of the string before the section that we want to delete a certain length 174 | 175 | //--------------------------- 176 | lenBeforeToDeleteIs307AsBytes := `{"1":"","0":"012345678901234567890123456789012345678901234567890123456789012345678901234567","to_delete":"0","2":""}` 177 | 178 | expectedForLenBefore307AsBytes := `{"1":"","0":"012345678901234567890123456789012345678901234567890123456789012345678901234567","2":""}` 179 | //--------------------------- 180 | 181 | //--------------------------- 182 | lenBeforeToDeleteIs308AsBytes := `{"1":"","0":"0123456789012345678901234567890123456789012345678901234567890123456789012345678","to_delete":"0","2":""}` 183 | 184 | expectedForLenBefore308AsBytes := `{"1":"","0":"0123456789012345678901234567890123456789012345678901234567890123456789012345678","2":""}` 185 | //--------------------------- 186 | 187 | //--------------------------- 188 | lenBeforeToDeleteIs309AsBytes := `{"1":"","0":"01234567890123456789012345678901234567890123456789012345678901234567890123456","to_delete":"0","2":""}` 189 | 190 | expectedForLenBefore309AsBytes := `{"1":"","0":"01234567890123456789012345678901234567890123456789012345678901234567890123456","2":""}` 191 | //--------------------------- 192 | 193 | var data = []struct { 194 | desc string 195 | input string 196 | expected string 197 | }{ 198 | { 199 | desc: "len before \"to_delete\"... = 307", 200 | input: lenBeforeToDeleteIs307AsBytes, 201 | expected: expectedForLenBefore307AsBytes, 202 | }, 203 | { 204 | desc: "len before \"to_delete\"... = 308", 205 | input: lenBeforeToDeleteIs308AsBytes, 206 | expected: expectedForLenBefore308AsBytes, 207 | }, 208 | { 209 | desc: "len before \"to_delete\"... = 309", 210 | input: lenBeforeToDeleteIs309AsBytes, 211 | expected: expectedForLenBefore309AsBytes, 212 | }, 213 | } 214 | 215 | for i, d := range data { 216 | result, err := Delete(d.input, "to_delete") 217 | 218 | if err != nil { 219 | t.Error(fmtErrorf(testError{ 220 | unexpected: "error", 221 | desc: d.desc, 222 | i: i, 223 | lenInput: len(d.input), 224 | input: d.input, 225 | expected: d.expected, 226 | result: result, 227 | })) 228 | } 229 | if result != d.expected { 230 | t.Error(fmtErrorf(testError{ 231 | unexpected: "result", 232 | desc: d.desc, 233 | i: i, 234 | lenInput: len(d.input), 235 | input: d.input, 236 | expected: d.expected, 237 | result: result, 238 | })) 239 | } 240 | } 241 | } 242 | 243 | type testError struct { 244 | unexpected string 245 | desc string 246 | i int 247 | lenInput int 248 | input interface{} 249 | expected interface{} 250 | result interface{} 251 | } 252 | 253 | func fmtErrorf(e testError) string { 254 | return fmt.Sprintf( 255 | "Unexpected %s:\n\t"+ 256 | "for=%q\n\t"+ 257 | "i=%d\n\t"+ 258 | "len(input)=%d\n\t"+ 259 | "input=%v\n\t"+ 260 | "expected=%v\n\t"+ 261 | "result=%v", 262 | e.unexpected, e.desc, e.i, e.lenInput, e.input, e.expected, e.result, 263 | ) 264 | } 265 | 266 | func TestSetDotKeyIssue10(t *testing.T) { 267 | json := `{"app.token":"abc"}` 268 | json, _ = Set(json, `app\.token`, "cde") 269 | if json != `{"app.token":"cde"}` { 270 | t.Fatalf("expected '%v', got '%v'", `{"app.token":"cde"}`, json) 271 | } 272 | } 273 | func TestDeleteDotKeyIssue19(t *testing.T) { 274 | json := []byte(`{"data":{"key1":"value1","key2.something":"value2"}}`) 275 | json, _ = DeleteBytes(json, `data.key2\.something`) 276 | if string(json) != `{"data":{"key1":"value1"}}` { 277 | t.Fatalf("expected '%v', got '%v'", `{"data":{"key1":"value1"}}`, json) 278 | } 279 | } 280 | 281 | func TestIssue36(t *testing.T) { 282 | var json = ` 283 | { 284 | "size": 1000 285 | } 286 | ` 287 | var raw = ` 288 | { 289 | "sample": "hello" 290 | } 291 | ` 292 | _ = raw 293 | if true { 294 | json, _ = SetRaw(json, "aggs", raw) 295 | } 296 | if !gjson.Valid(json) { 297 | t.Fatal("invalid json") 298 | } 299 | res := gjson.Get(json, "aggs.sample").String() 300 | if res != "hello" { 301 | t.Fatal("unexpected result") 302 | } 303 | } 304 | 305 | var example = ` 306 | { 307 | "name": {"first": "Tom", "last": "Anderson"}, 308 | "age":37, 309 | "children": ["Sara","Alex","Jack"], 310 | "fav.movie": "Deer Hunter", 311 | "friends": [ 312 | {"first": "Dale", "last": "Murphy", "age": 44, "nets": ["ig", "fb", "tw"]}, 313 | {"first": "Roger", "last": "Craig", "age": 68, "nets": ["fb", "tw"]}, 314 | {"first": "Jane", "last": "Murphy", "age": 47, "nets": ["ig", "tw"]} 315 | ] 316 | } 317 | ` 318 | 319 | func TestIndex(t *testing.T) { 320 | path := `friends.#(last="Murphy").last` 321 | json, err := Set(example, path, "Johnson") 322 | if err != nil { 323 | t.Fatal(err) 324 | } 325 | if gjson.Get(json, "friends.#.last").String() != `["Johnson","Craig","Murphy"]` { 326 | t.Fatal("mismatch") 327 | } 328 | } 329 | 330 | func TestIndexes(t *testing.T) { 331 | path := `friends.#(last="Murphy")#.last` 332 | json, err := Set(example, path, "Johnson") 333 | if err != nil { 334 | t.Fatal(err) 335 | } 336 | if gjson.Get(json, "friends.#.last").String() != `["Johnson","Craig","Johnson"]` { 337 | t.Fatal("mismatch") 338 | } 339 | } 340 | 341 | func TestIssue61(t *testing.T) { 342 | json := `{ 343 | "@context": { 344 | "rdfs": "http://www.w3.org/2000/01/rdf-schema#", 345 | "@vocab": "http://schema.org/", 346 | "sh": "http://www.w3.org/ns/shacl#" 347 | } 348 | }` 349 | json1, _ := Set(json, "@context.@vocab", "newval") 350 | if gjson.Get(json1, "@context.@vocab").String() != "newval" { 351 | t.Fail() 352 | } 353 | } 354 | --------------------------------------------------------------------------------