├── .gitignore ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── gojq.go └── gojq_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | .DS_Store 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | #License 2 | ISC License (ISC) 3 | 4 | Copyright (c) 2014, Elgs Qian Chen 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 11 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 12 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 13 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 14 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 15 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 16 | PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gojq 2 | JSON query in Golang. 3 | 4 | ## Install 5 | `go get -u github.com/elgs/gojq` 6 | 7 | This library serves three purposes: 8 | 9 | * makes parsing JSON configuration file much easier 10 | * enables JSON expression evaluation 11 | * reduces the pain of type assertion parsing JSON 12 | 13 | 14 | ## Query from JSON Object 15 | ```go 16 | package main 17 | 18 | import ( 19 | "fmt" 20 | 21 | "github.com/elgs/gojq" 22 | ) 23 | 24 | var jsonObj = ` 25 | { 26 | "name": "sam", 27 | "gender": "m", 28 | "pet": null, 29 | "skills": [ 30 | "Eating", 31 | "Sleeping", 32 | "Crawling" 33 | ], 34 | "hello.world":true 35 | } 36 | ` 37 | 38 | func main() { 39 | parser, err := gojq.NewStringQuery(jsonObj) 40 | if err != nil { 41 | fmt.Println(err) 42 | return 43 | } 44 | fmt.Println(parser.Query("name")) // sam 45 | fmt.Println(parser.Query("gender")) // m 46 | fmt.Println(parser.Query("skills.[1]")) // Sleeping 47 | fmt.Println(parser.Query("hello")) // hello does not exist. 48 | fmt.Println(parser.Query("pet")) // 49 | fmt.Println(parser.Query(".")) // map[name:sam gender:m pet: skills:[Eating Sleeping Crawling] hello.world:true] 50 | fmt.Println(parser.Query("'hello.world'")) // true 51 | } 52 | 53 | ``` 54 | 55 | ## Query from JSON Array 56 | ```go 57 | package main 58 | 59 | import ( 60 | "fmt" 61 | "github.com/elgs/gojq" 62 | ) 63 | 64 | var jsonArray = ` 65 | [ 66 | { 67 | "name": "elgs", 68 | "gender": "m", 69 | "skills": [ 70 | "Golang", 71 | "Java", 72 | "C" 73 | ] 74 | }, 75 | { 76 | "name": "enny", 77 | "gender": "f", 78 | "skills": [ 79 | "IC", 80 | "Electric design", 81 | "Verification" 82 | ] 83 | }, 84 | { 85 | "name": "sam", 86 | "gender": "m", 87 | "pet": null, 88 | "skills": [ 89 | "Eating", 90 | "Sleeping", 91 | "Crawling" 92 | ] 93 | } 94 | ] 95 | ` 96 | 97 | func main() { 98 | parser, err := gojq.NewStringQuery(jsonArray) 99 | if err != nil { 100 | fmt.Println(err) 101 | return 102 | } 103 | fmt.Println(parser.Query("[0].name")) // elgs 104 | fmt.Println(parser.Query("[1].gender")) // f 105 | fmt.Println(parser.Query("[2].skills.[1]")) // Sleeping 106 | fmt.Println(parser.Query("[2].hello")) // hello does not exist. 107 | fmt.Println(parser.Query("[2].pet")) // 108 | } 109 | ``` 110 | ## Nested Query 111 | ```go 112 | package main 113 | 114 | import ( 115 | "fmt" 116 | "github.com/elgs/gojq" 117 | ) 118 | 119 | var jsonArray = ` 120 | [ 121 | { 122 | "name": "elgs", 123 | "gender": "m", 124 | "skills": [ 125 | "Golang", 126 | "Java", 127 | "C" 128 | ] 129 | }, 130 | { 131 | "name": "enny", 132 | "gender": "f", 133 | "skills": [ 134 | "IC", 135 | "Electric design", 136 | "Verification" 137 | ] 138 | }, 139 | { 140 | "name": "sam", 141 | "gender": "m", 142 | "pet": null, 143 | "skills": [ 144 | "Eating", 145 | "Sleeping", 146 | "Crawling" 147 | ] 148 | } 149 | ] 150 | ` 151 | 152 | func main() { 153 | parser, err := gojq.NewStringQuery(jsonArray) 154 | if err != nil { 155 | fmt.Println(err) 156 | return 157 | } 158 | samSkills, err := parser.Query("[2].skills") 159 | fmt.Println(samSkills, err) //[Eating Sleeping Crawling] 160 | samSkillParser := gojq.NewQuery(samSkills) 161 | fmt.Println(samSkillParser.Query("[1]")) //Sleeping 162 | } 163 | ``` 164 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/elgs/gojq 2 | 3 | go 1.20 4 | 5 | require github.com/elgs/gosplitargs v0.0.0-20230310130726-7d16e488436a 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/elgs/gosplitargs v0.0.0-20230310130726-7d16e488436a h1:vO9yvZPJqJMJJsFdaD+sLkwjZfxzr/0fm22AnW8R6ms= 3 | github.com/elgs/gosplitargs v0.0.0-20230310130726-7d16e488436a/go.mod h1:1zHirgOZTAY00iwXKAX0NsVKjwkwjSr1P+yqciHfRH0= 4 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 5 | github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= 6 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 7 | -------------------------------------------------------------------------------- /gojq.go: -------------------------------------------------------------------------------- 1 | package gojq 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "strconv" 9 | "strings" 10 | 11 | "github.com/elgs/gosplitargs" 12 | ) 13 | 14 | // JQ (JSON Query) struct 15 | type JQ struct { 16 | Data any 17 | } 18 | 19 | // NewFileQuery - Create a new &JQ from a JSON file. 20 | func NewFileQuery(jsonFile string) (*JQ, error) { 21 | raw, err := os.ReadFile(jsonFile) 22 | if err != nil { 23 | return nil, err 24 | } 25 | var data = new(any) 26 | err = json.Unmarshal(raw, data) 27 | if err != nil { 28 | return nil, err 29 | } 30 | return &JQ{*data}, nil 31 | } 32 | 33 | // NewStringQuery - Create a new &JQ from a raw JSON string. 34 | func NewStringQuery(jsonString string) (*JQ, error) { 35 | var data = new(any) 36 | err := json.Unmarshal([]byte(jsonString), data) 37 | if err != nil { 38 | return nil, err 39 | } 40 | return &JQ{*data}, nil 41 | } 42 | 43 | // NewQuery - Create a &JQ from an any parsed by json.Unmarshal 44 | func NewQuery(jsonObject any) *JQ { 45 | return &JQ{Data: jsonObject} 46 | } 47 | 48 | // Query queries against the JSON with the expression passed in. The exp is separated by dots (".") 49 | func (jq *JQ) Query(exp string) (any, error) { 50 | if exp == "." { 51 | return jq.Data, nil 52 | } 53 | paths, err := gosplitargs.SplitArgs(exp, ".", false) 54 | if err != nil { 55 | return nil, err 56 | } 57 | var context any = jq.Data 58 | for _, path := range paths { 59 | if len(path) >= 3 && strings.HasPrefix(path, "[") && strings.HasSuffix(path, "]") { 60 | // array 61 | index, err := strconv.Atoi(path[1 : len(path)-1]) 62 | if err != nil { 63 | return nil, err 64 | } 65 | if v, ok := context.([]any); ok { 66 | if len(v) <= index { 67 | return nil, errors.New(fmt.Sprint(path, " index out of range.")) 68 | } 69 | context = v[index] 70 | } else { 71 | return nil, errors.New(fmt.Sprint(path, " is not an array. ", v)) 72 | } 73 | } else { 74 | // map 75 | if v, ok := context.(map[string]any); ok { 76 | if val, ok := v[path]; ok { 77 | context = val 78 | } else { 79 | return nil, errors.New(fmt.Sprint(path, " does not exist.")) 80 | } 81 | } else { 82 | return nil, errors.New(fmt.Sprint(path, " is not an object. ", v)) 83 | } 84 | } 85 | } 86 | return context, nil 87 | } 88 | 89 | // QueryToMap queries against the JSON with the expression passed in, and convert to a map[string]any 90 | func (jq *JQ) QueryToMap(exp string) (map[string]any, error) { 91 | r, err := jq.Query(exp) 92 | if err != nil { 93 | return nil, errors.New("Failed to parse: " + exp) 94 | } 95 | if ret, ok := r.(map[string]any); ok { 96 | return ret, nil 97 | } 98 | return nil, errors.New("Failed to convert to map: " + exp) 99 | } 100 | 101 | // QueryToMap queries against the JSON with the expression passed in, and convert to a array: []any 102 | func (jq *JQ) QueryToArray(exp string) ([]any, error) { 103 | r, err := jq.Query(exp) 104 | if err != nil { 105 | return nil, errors.New("Failed to parse: " + exp) 106 | } 107 | if ret, ok := r.([]any); ok { 108 | return ret, nil 109 | } 110 | return nil, errors.New("Failed to convert to array: " + exp) 111 | } 112 | 113 | // QueryToMap queries against the JSON with the expression passed in, and convert to string 114 | func (jq *JQ) QueryToString(exp string) (string, error) { 115 | r, err := jq.Query(exp) 116 | if err != nil { 117 | return "", errors.New("Failed to parse: " + exp) 118 | } 119 | if ret, ok := r.(string); ok { 120 | return ret, nil 121 | } 122 | return "", errors.New("Failed to convert to string: " + exp) 123 | } 124 | 125 | // QueryToMap queries against the JSON with the expression passed in, and convert to int64 126 | func (jq *JQ) QueryToInt64(exp string) (int64, error) { 127 | r, err := jq.Query(exp) 128 | if err != nil { 129 | return 0, errors.New("Failed to parse: " + exp) 130 | } 131 | if ret, ok := r.(float64); ok { 132 | return int64(ret), nil 133 | } 134 | return 0, errors.New("Failed to convert to int64: " + exp) 135 | } 136 | 137 | // QueryToMap queries against the JSON with the expression passed in, and convert to float64 138 | func (jq *JQ) QueryToFloat64(exp string) (float64, error) { 139 | r, err := jq.Query(exp) 140 | if err != nil { 141 | return 0, errors.New("Failed to parse: " + exp) 142 | } 143 | if ret, ok := r.(float64); ok { 144 | return ret, nil 145 | } 146 | return 0, errors.New("Failed to convert to float64: " + exp) 147 | } 148 | 149 | // QueryToMap queries against the JSON with the expression passed in, and convert to bool 150 | func (jq *JQ) QueryToBool(exp string) (bool, error) { 151 | r, err := jq.Query(exp) 152 | if err != nil { 153 | return false, errors.New("Failed to parse: " + exp) 154 | } 155 | if ret, ok := r.(bool); ok { 156 | return ret, nil 157 | } 158 | return false, errors.New("Failed to convert to float64: " + exp) 159 | } 160 | -------------------------------------------------------------------------------- /gojq_test.go: -------------------------------------------------------------------------------- 1 | package gojq 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | var jsonArray = ` 8 | [ 9 | { 10 | "name": "elgs", 11 | "gender": "m", 12 | "skills": [ 13 | "Golang", 14 | "Java", 15 | "C" 16 | ] 17 | }, 18 | { 19 | "name": "enny", 20 | "gender": "f", 21 | "skills": [ 22 | "IC", 23 | "Electric design", 24 | "Verification" 25 | ] 26 | }, 27 | { 28 | "name": "sam", 29 | "gender": "m", 30 | "skills": [ 31 | "Eating", 32 | "Sleeping", 33 | "Crawling" 34 | ], 35 | "hello": null, 36 | "hello.world":true 37 | } 38 | ] 39 | ` 40 | 41 | func TestParseJsonArray(t *testing.T) { 42 | 43 | parserArray, err := NewStringQuery(jsonArray) 44 | if err != nil { 45 | t.Error(err) 46 | } 47 | 48 | var pass = []struct { 49 | in string 50 | ex interface{} 51 | }{ 52 | {"[0].name", "elgs"}, 53 | {"[1].gender", "f"}, 54 | {"[2].skills.[1]", "Sleeping"}, 55 | {"[2].hello", nil}, 56 | {"[2].'hello.world'", true}, 57 | } 58 | var fail = []struct { 59 | in string 60 | ex interface{} 61 | }{} 62 | for _, v := range pass { 63 | result, err := parserArray.Query(v.in) 64 | if err != nil { 65 | t.Error(err) 66 | } 67 | if v.ex != result { 68 | t.Error("Expected:", v.ex, "actual:", result) 69 | } 70 | } 71 | for range fail { 72 | 73 | } 74 | } 75 | 76 | var jsonObj = ` 77 | { 78 | "name": "sam", 79 | "gender": "m", 80 | "skills": [ 81 | "Eating", 82 | "Sleeping", 83 | "Crawling" 84 | ], 85 | "hello":null 86 | } 87 | ` 88 | 89 | func TestParseJsonObj(t *testing.T) { 90 | 91 | parserObj, err := NewStringQuery(jsonObj) 92 | if err != nil { 93 | t.Error(err) 94 | } 95 | 96 | var pass = []struct { 97 | in string 98 | ex interface{} 99 | }{ 100 | {"name", "sam"}, 101 | {"gender", "m"}, 102 | {"skills.[1]", "Sleeping"}, 103 | {"hello", nil}, 104 | } 105 | var fail = []struct { 106 | in string 107 | ex interface{} 108 | }{} 109 | for _, v := range pass { 110 | result, err := parserObj.Query(v.in) 111 | if err != nil { 112 | t.Error(err) 113 | } 114 | if v.ex != result { 115 | t.Error("Expected:", v.ex, "actual:", result) 116 | } 117 | } 118 | for range fail { 119 | 120 | } 121 | } 122 | --------------------------------------------------------------------------------