├── .gitignore ├── LICENSE ├── README.md ├── autotest.sh ├── doc.go ├── jsonq.go └── jsonq_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.sw[op] 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Jason Moiron 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsonq 2 | 3 | [![Build Status](https://drone.io/github.com/jmoiron/jsonq/status.png)](https://drone.io/github.com/jmoiron/jsonq/latest) [![Godoc](http://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/github.com/jmoiron/jsonq) [![license](http://img.shields.io/badge/license-MIT-red.svg?style=flat)](https://raw.githubusercontent.com/jmoiron/jsonq/master/LICENSE) 4 | 5 | 6 | Simplify your golang json usage by extracting fields or items from arrays and objects with a simple, hierarchical query. [API Documentation](http://godoc.org/github.com/jmoiron/jsonq) on godoc.org. 7 | 8 | This package is meant to make working with complex feeds a bit more easy. If you have simple feeds you want to model with struct types, check out [jflect](http://github.com/str1ngs/jflect), which will create struct definitions given a json document. 9 | 10 | # installing 11 | 12 | ``` 13 | go get github.com/jmoiron/jsonq 14 | ``` 15 | 16 | # usage 17 | 18 | Given some json data like: 19 | 20 | ```javascript 21 | { 22 | "foo": 1, 23 | "bar": 2, 24 | "test": "Hello, world!", 25 | "baz": 123.1, 26 | "array": [ 27 | {"foo": 1}, 28 | {"bar": 2}, 29 | {"baz": 3} 30 | ], 31 | "subobj": { 32 | "foo": 1, 33 | "subarray": [1,2,3], 34 | "subsubobj": { 35 | "bar": 2, 36 | "baz": 3, 37 | "array": ["hello", "world"] 38 | } 39 | }, 40 | "bool": true 41 | } 42 | ``` 43 | 44 | Decode it into a `map[string]interface{}`: 45 | 46 | ```go 47 | import ( 48 | "strings" 49 | "encoding/json" 50 | "github.com/jmoiron/jsonq" 51 | ) 52 | 53 | data := map[string]interface{}{} 54 | dec := json.NewDecoder(strings.NewReader(jsonstring)) 55 | dec.Decode(&data) 56 | jq := jsonq.NewQuery(data) 57 | ``` 58 | 59 | From here, you can query along different keys and indexes: 60 | 61 | ```go 62 | // data["foo"] -> 1 63 | jq.Int("foo") 64 | 65 | // data["subobj"]["subarray"][1] -> 2 66 | jq.Int("subobj", "subarray", "1") 67 | 68 | // data["subobj"]["subarray"]["array"][0] -> "hello" 69 | jq.String("subobj", "subsubobj", "array", "0") 70 | 71 | // data["subobj"] -> map[string]interface{}{"subobj": ...} 72 | obj, err := jq.Object("subobj") 73 | ``` 74 | 75 | Missing keys, out of bounds indexes, and type failures will return errors. 76 | For simplicity, integer keys (ie, {"0": "zero"}) are inaccessible 77 | by `jsonq` as integer strings are assumed to be array indexes. 78 | 79 | The `Int` and `Float` methods will attempt to parse numbers from string 80 | values to ease the use of many real world feeds which deliver numbers as strings. 81 | 82 | Suggestions/comments please tweet [@jmoiron](http://twitter.com/jmoiron) 83 | 84 | -------------------------------------------------------------------------------- /autotest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cur=`pwd` 4 | 5 | inotifywait -mqr --timefmt '%d/%m/%y %H:%M' --format '%T %w %f' \ 6 | -e modify ./ | while read date time dir file; do 7 | ext="${file##*.}" 8 | if [[ "$ext" = "go" ]]; then 9 | echo "$file changed @ $time $date, rebuilding..." 10 | go test 11 | fi 12 | done 13 | 14 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package jsonq simplify your json usage with a simple hierarchical query. 3 | 4 | Given some json data like: 5 | 6 | { 7 | "foo": 1, 8 | "bar": 2, 9 | "test": "Hello, world!", 10 | "baz": 123.1, 11 | "array": [ 12 | {"foo": 1}, 13 | {"bar": 2}, 14 | {"baz": 3} 15 | ], 16 | "subobj": { 17 | "foo": 1, 18 | "subarray": [1,2,3], 19 | "subsubobj": { 20 | "bar": 2, 21 | "baz": 3, 22 | "array": ["hello", "world"] 23 | } 24 | }, 25 | "bool": true 26 | } 27 | 28 | Decode it into a map[string]interrface{}: 29 | 30 | import ( 31 | "strings" 32 | "encoding/json" 33 | "github.com/jmoiron/jsonq" 34 | ) 35 | 36 | data := map[string]interface{}{} 37 | dec := json.NewDecoder(strings.NewReader(jsonstring)) 38 | dec.Decode(&data) 39 | jq := jsonq.NewQuery(data) 40 | 41 | From here, you can query along different keys and indexes: 42 | 43 | // data["foo"] -> 1 44 | jq.Int("foo") 45 | 46 | // data["subobj"]["subarray"][1] -> 2 47 | jq.Int("subobj", "subarray", "1") 48 | 49 | // data["subobj"]["subarray"]["array"][0] -> "hello" 50 | jq.String("subobj", "subsubobj", "array", "0") 51 | 52 | // data["subobj"] -> map[string]interface{}{"subobj": ...} 53 | obj, err := jq.Object("subobj") 54 | 55 | Notes: 56 | 57 | Missing keys, out of bounds indexes, and type failures will return errors. 58 | For simplicity, integer keys (ie, {"0": "zero"}) are inaccessible by `jsonq` 59 | as integer strings are assumed to be array indexes. 60 | 61 | */ 62 | package jsonq 63 | -------------------------------------------------------------------------------- /jsonq.go: -------------------------------------------------------------------------------- 1 | package jsonq 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | ) 7 | 8 | // JsonQuery is an object that enables querying of a Go map with a simple 9 | // positional query language. 10 | type JsonQuery struct { 11 | blob map[string]interface{} 12 | } 13 | 14 | // stringFromInterface converts an interface{} to a string and returns an error if types don't match. 15 | func stringFromInterface(val interface{}) (string, error) { 16 | switch val.(type) { 17 | case string: 18 | return val.(string), nil 19 | } 20 | return "", fmt.Errorf("Expected string value for String, got \"%v\"\n", val) 21 | } 22 | 23 | // boolFromInterface converts an interface{} to a bool and returns an error if types don't match. 24 | func boolFromInterface(val interface{}) (bool, error) { 25 | switch val.(type) { 26 | case bool: 27 | return val.(bool), nil 28 | } 29 | return false, fmt.Errorf("Expected boolean value for Bool, got \"%v\"\n", val) 30 | } 31 | 32 | // floatFromInterface converts an interface{} to a float64 and returns an error if types don't match. 33 | func floatFromInterface(val interface{}) (float64, error) { 34 | switch val.(type) { 35 | case float64: 36 | return val.(float64), nil 37 | case int: 38 | return float64(val.(int)), nil 39 | case string: 40 | fval, err := strconv.ParseFloat(val.(string), 64) 41 | if err == nil { 42 | return fval, nil 43 | } 44 | } 45 | return 0.0, fmt.Errorf("Expected numeric value for Float, got \"%v\"\n", val) 46 | } 47 | 48 | // intFromInterface converts an interface{} to an int and returns an error if types don't match. 49 | func intFromInterface(val interface{}) (int, error) { 50 | switch val.(type) { 51 | case float64: 52 | return int(val.(float64)), nil 53 | case string: 54 | ival, err := strconv.ParseFloat(val.(string), 64) 55 | if err == nil { 56 | return int(ival), nil 57 | } 58 | case int: 59 | return val.(int), nil 60 | } 61 | return 0, fmt.Errorf("Expected numeric value for Int, got \"%v\"\n", val) 62 | } 63 | 64 | // objectFromInterface converts an interface{} to a map[string]interface{} and returns an error if types don't match. 65 | func objectFromInterface(val interface{}) (map[string]interface{}, error) { 66 | switch val.(type) { 67 | case map[string]interface{}: 68 | return val.(map[string]interface{}), nil 69 | } 70 | return map[string]interface{}{}, fmt.Errorf("Expected json object for Object, got \"%v\"\n", val) 71 | } 72 | 73 | // arrayFromInterface converts an interface{} to an []interface{} and returns an error if types don't match. 74 | func arrayFromInterface(val interface{}) ([]interface{}, error) { 75 | switch val.(type) { 76 | case []interface{}: 77 | return val.([]interface{}), nil 78 | } 79 | return []interface{}{}, fmt.Errorf("Expected json array for Array, got \"%v\"\n", val) 80 | } 81 | 82 | // NewQuery creates a new JsonQuery obj from an interface{}. 83 | func NewQuery(data interface{}) *JsonQuery { 84 | j := new(JsonQuery) 85 | j.blob = data.(map[string]interface{}) 86 | return j 87 | } 88 | 89 | // Bool extracts a bool the JsonQuery 90 | func (j *JsonQuery) Bool(s ...string) (bool, error) { 91 | val, err := rquery(j.blob, s...) 92 | if err != nil { 93 | return false, err 94 | } 95 | return boolFromInterface(val) 96 | } 97 | 98 | // Float extracts a float from the JsonQuery 99 | func (j *JsonQuery) Float(s ...string) (float64, error) { 100 | val, err := rquery(j.blob, s...) 101 | if err != nil { 102 | return 0.0, err 103 | } 104 | return floatFromInterface(val) 105 | } 106 | 107 | // Int extracts an int from the JsonQuery 108 | func (j *JsonQuery) Int(s ...string) (int, error) { 109 | val, err := rquery(j.blob, s...) 110 | if err != nil { 111 | return 0, err 112 | } 113 | return intFromInterface(val) 114 | } 115 | 116 | // String extracts a string from the JsonQuery 117 | func (j *JsonQuery) String(s ...string) (string, error) { 118 | val, err := rquery(j.blob, s...) 119 | if err != nil { 120 | return "", err 121 | } 122 | return stringFromInterface(val) 123 | } 124 | 125 | // Object extracts a json object from the JsonQuery 126 | func (j *JsonQuery) Object(s ...string) (map[string]interface{}, error) { 127 | val, err := rquery(j.blob, s...) 128 | if err != nil { 129 | return map[string]interface{}{}, err 130 | } 131 | return objectFromInterface(val) 132 | } 133 | 134 | // Array extracts a []interface{} from the JsonQuery 135 | func (j *JsonQuery) Array(s ...string) ([]interface{}, error) { 136 | val, err := rquery(j.blob, s...) 137 | if err != nil { 138 | return []interface{}{}, err 139 | } 140 | return arrayFromInterface(val) 141 | } 142 | 143 | // Interface extracts an interface{} from the JsonQuery 144 | func (j *JsonQuery) Interface(s ...string) (interface{}, error) { 145 | val, err := rquery(j.blob, s...) 146 | if err != nil { 147 | return nil, err 148 | } 149 | return val, nil 150 | } 151 | 152 | // ArrayOfStrings extracts an array of strings from some json 153 | func (j *JsonQuery) ArrayOfStrings(s ...string) ([]string, error) { 154 | array, err := j.Array(s...) 155 | if err != nil { 156 | return []string{}, err 157 | } 158 | toReturn := make([]string, len(array)) 159 | for index, val := range array { 160 | toReturn[index], err = stringFromInterface(val) 161 | if err != nil { 162 | return toReturn, err 163 | } 164 | } 165 | return toReturn, nil 166 | } 167 | 168 | // ArrayOfInts extracts an array of ints from some json 169 | func (j *JsonQuery) ArrayOfInts(s ...string) ([]int, error) { 170 | array, err := j.Array(s...) 171 | if err != nil { 172 | return []int{}, err 173 | } 174 | toReturn := make([]int, len(array)) 175 | for index, val := range array { 176 | toReturn[index], err = intFromInterface(val) 177 | if err != nil { 178 | return toReturn, err 179 | } 180 | } 181 | return toReturn, nil 182 | } 183 | 184 | // ArrayOfFloats extracts an array of float64s from some json 185 | func (j *JsonQuery) ArrayOfFloats(s ...string) ([]float64, error) { 186 | array, err := j.Array(s...) 187 | if err != nil { 188 | return []float64{}, err 189 | } 190 | toReturn := make([]float64, len(array)) 191 | for index, val := range array { 192 | toReturn[index], err = floatFromInterface(val) 193 | if err != nil { 194 | return toReturn, err 195 | } 196 | } 197 | return toReturn, nil 198 | } 199 | 200 | // ArrayOfBools extracts an array of bools from some json 201 | func (j *JsonQuery) ArrayOfBools(s ...string) ([]bool, error) { 202 | array, err := j.Array(s...) 203 | if err != nil { 204 | return []bool{}, err 205 | } 206 | toReturn := make([]bool, len(array)) 207 | for index, val := range array { 208 | toReturn[index], err = boolFromInterface(val) 209 | if err != nil { 210 | return toReturn, err 211 | } 212 | } 213 | return toReturn, nil 214 | } 215 | 216 | // ArrayOfObjects extracts an array of map[string]interface{} (objects) from some json 217 | func (j *JsonQuery) ArrayOfObjects(s ...string) ([]map[string]interface{}, error) { 218 | array, err := j.Array(s...) 219 | if err != nil { 220 | return []map[string]interface{}{}, err 221 | } 222 | toReturn := make([]map[string]interface{}, len(array)) 223 | for index, val := range array { 224 | toReturn[index], err = objectFromInterface(val) 225 | if err != nil { 226 | return toReturn, err 227 | } 228 | } 229 | return toReturn, nil 230 | } 231 | 232 | // ArrayOfArrays extracts an array of []interface{} (arrays) from some json 233 | func (j *JsonQuery) ArrayOfArrays(s ...string) ([][]interface{}, error) { 234 | array, err := j.Array(s...) 235 | if err != nil { 236 | return [][]interface{}{}, err 237 | } 238 | toReturn := make([][]interface{}, len(array)) 239 | for index, val := range array { 240 | toReturn[index], err = arrayFromInterface(val) 241 | if err != nil { 242 | return toReturn, err 243 | } 244 | } 245 | return toReturn, nil 246 | } 247 | 248 | // Matrix2D is an alias for ArrayOfArrays 249 | func (j *JsonQuery) Matrix2D(s ...string) ([][]interface{}, error) { 250 | return j.ArrayOfArrays(s...) 251 | } 252 | 253 | // Recursively query a decoded json blob 254 | func rquery(blob interface{}, s ...string) (interface{}, error) { 255 | var ( 256 | val interface{} 257 | err error 258 | ) 259 | val = blob 260 | for _, q := range s { 261 | val, err = query(val, q) 262 | if err != nil { 263 | return nil, err 264 | } 265 | } 266 | switch val.(type) { 267 | case nil: 268 | return nil, fmt.Errorf("Nil value found at %s\n", s[len(s)-1]) 269 | } 270 | return val, nil 271 | } 272 | 273 | // query a json blob for a single field or index. If query is a string, then 274 | // the blob is treated as a json object (map[string]interface{}). If query is 275 | // an integer, the blob is treated as a json array ([]interface{}). Any kind 276 | // of key or index error will result in a nil return value with an error set. 277 | func query(blob interface{}, query string) (interface{}, error) { 278 | index, err := strconv.Atoi(query) 279 | // if it's an integer, then we treat the current interface as an array 280 | if err == nil { 281 | switch blob.(type) { 282 | case []interface{}: 283 | default: 284 | return nil, fmt.Errorf("Array index on non-array %v\n", blob) 285 | } 286 | if len(blob.([]interface{})) > index { 287 | return blob.([]interface{})[index], nil 288 | } 289 | return nil, fmt.Errorf("Array index %d on array %v out of bounds\n", index, blob) 290 | } 291 | 292 | // blob is likely an object, but verify first 293 | switch blob.(type) { 294 | case map[string]interface{}: 295 | default: 296 | return nil, fmt.Errorf("Object lookup \"%s\" on non-object %v\n", query, blob) 297 | } 298 | 299 | val, ok := blob.(map[string]interface{})[query] 300 | if !ok { 301 | return nil, fmt.Errorf("Object %v does not contain field %s\n", blob, query) 302 | } 303 | return val, nil 304 | } 305 | -------------------------------------------------------------------------------- /jsonq_test.go: -------------------------------------------------------------------------------- 1 | package jsonq 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | const TestData = `{ 11 | "foo": 1, 12 | "bar": 2, 13 | "test": "Hello, world!", 14 | "baz": 123.1, 15 | "numstring": "42", 16 | "floatstring": "42.1", 17 | "array": [ 18 | {"foo": 1}, 19 | {"bar": 2}, 20 | {"baz": 3} 21 | ], 22 | "subobj": { 23 | "foo": 1, 24 | "subarray": [1,2,3], 25 | "subsubobj": { 26 | "bar": 2, 27 | "baz": 3, 28 | "array": ["hello", "world"] 29 | } 30 | }, 31 | "collections": { 32 | "bools": [false, true, false], 33 | "strings": ["hello", "strings"], 34 | "numbers": [1,2,3,4], 35 | "arrays": [[1.0,2.0],[2.0,3.0],[4.0,3.0]], 36 | "objects": [ 37 | {"obj1": 1}, 38 | {"obj2": 2} 39 | ] 40 | }, 41 | "bool": true 42 | }` 43 | 44 | func tErr(t *testing.T, err error) { 45 | if err != nil { 46 | t.Errorf("Error: %v\n", err) 47 | } 48 | } 49 | 50 | func TestQuery(t *testing.T) { 51 | data := map[string]interface{}{} 52 | dec := json.NewDecoder(strings.NewReader(TestData)) 53 | err := dec.Decode(&data) 54 | tErr(t, err) 55 | q := NewQuery(data) 56 | 57 | ival, err := q.Int("foo") 58 | if ival != 1 { 59 | t.Errorf("Expecting 1, got %v\n", ival) 60 | } 61 | tErr(t, err) 62 | ival, err = q.Int("bar") 63 | if ival != 2 { 64 | t.Errorf("Expecting 2, got %v\n", ival) 65 | } 66 | tErr(t, err) 67 | 68 | ival, err = q.Int("subobj", "foo") 69 | if ival != 1 { 70 | t.Errorf("Expecting 1, got %v\n", ival) 71 | } 72 | tErr(t, err) 73 | 74 | // test that strings can get int-ed 75 | ival, err = q.Int("numstring") 76 | if ival != 42 { 77 | t.Errorf("Expecting 42, got %v\n", ival) 78 | } 79 | tErr(t, err) 80 | 81 | for i := 0; i < 3; i++ { 82 | ival, err := q.Int("subobj", "subarray", fmt.Sprintf("%d", i)) 83 | if ival != i+1 { 84 | t.Errorf("Expecting %d, got %v\n", i+1, ival) 85 | } 86 | tErr(t, err) 87 | } 88 | 89 | fval, err := q.Float("baz") 90 | if fval != 123.1 { 91 | t.Errorf("Expecting 123.1, got %f\n", fval) 92 | } 93 | tErr(t, err) 94 | 95 | // test that strings can get float-ed 96 | fval, err = q.Float("floatstring") 97 | if fval != 42.1 { 98 | t.Errorf("Expecting 42.1, got %v\n", fval) 99 | } 100 | tErr(t, err) 101 | 102 | sval, err := q.String("test") 103 | if sval != "Hello, world!" { 104 | t.Errorf("Expecting \"Hello, World!\", got \"%v\"\n", sval) 105 | } 106 | 107 | sval, err = q.String("subobj", "subsubobj", "array", "0") 108 | if sval != "hello" { 109 | t.Errorf("Expecting \"hello\", got \"%s\"\n", sval) 110 | } 111 | tErr(t, err) 112 | 113 | bval, err := q.Bool("bool") 114 | if !bval { 115 | t.Errorf("Expecting true, got %v\n", bval) 116 | } 117 | tErr(t, err) 118 | 119 | obj, err := q.Object("subobj", "subsubobj") 120 | tErr(t, err) 121 | q2 := NewQuery(obj) 122 | sval, err = q2.String("array", "1") 123 | if sval != "world" { 124 | t.Errorf("Expecting \"world\", got \"%s\"\n", sval) 125 | } 126 | tErr(t, err) 127 | 128 | aobj, err := q.Array("subobj", "subarray") 129 | tErr(t, err) 130 | if aobj[0].(float64) != 1 { 131 | t.Errorf("Expecting 1, got %v\n", aobj[0]) 132 | } 133 | 134 | iobj, err := q.Interface("numstring") 135 | tErr(t, err) 136 | if _, ok := iobj.(string); !ok { 137 | t.Errorf("Expecting type string got: %s", iobj) 138 | } 139 | 140 | /* 141 | Test Extraction of typed slices 142 | */ 143 | 144 | //test array of strings 145 | astrings, err := q.ArrayOfStrings("collections", "strings") 146 | tErr(t, err) 147 | if astrings[0] != "hello" { 148 | t.Errorf("Expecting hello, got %v\n", astrings[0]) 149 | } 150 | 151 | //test array of ints 152 | aints, err := q.ArrayOfInts("collections", "numbers") 153 | tErr(t, err) 154 | if aints[0] != 1 { 155 | t.Errorf("Expecting 1, got %v\n", aints[0]) 156 | } 157 | 158 | //test array of floats 159 | afloats, err := q.ArrayOfFloats("collections", "numbers") 160 | tErr(t, err) 161 | if afloats[0] != 1.0 { 162 | t.Errorf("Expecting 1.0, got %v\n", afloats[0]) 163 | } 164 | 165 | //test array of bools 166 | abools, err := q.ArrayOfBools("collections", "bools") 167 | tErr(t, err) 168 | if abools[0] { 169 | t.Errorf("Expecting true, got %v\n", abools[0]) 170 | } 171 | 172 | //test array of arrays 173 | aa, err := q.ArrayOfArrays("collections", "arrays") 174 | tErr(t, err) 175 | if aa[0][0].(float64) != 1 { 176 | t.Errorf("Expecting 1, got %v\n", aa[0][0]) 177 | } 178 | 179 | //test array of objs 180 | aobjs, err := q.ArrayOfObjects("collections", "objects") 181 | tErr(t, err) 182 | if aobjs[0]["obj1"].(float64) != 1 { 183 | t.Errorf("Expecting 1, got %v\n", aobjs[0]["obj1"]) 184 | } 185 | 186 | } 187 | --------------------------------------------------------------------------------