├── go.mod ├── .github └── workflows │ └── test.yml ├── example └── simple.go ├── go.sum ├── LICENSE ├── README.md ├── jsonschema.go └── jsonschema_test.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mcuadros/go-jsonschema-generator 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect 7 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f 8 | ) 9 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Test 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Install Go 8 | uses: actions/setup-go@v1 9 | with: 10 | go-version: 1.14.x 11 | - name: Checkout code 12 | uses: actions/checkout@v2 13 | - name: Test 14 | run: go test ./... -------------------------------------------------------------------------------- /example/simple.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/mcuadros/go-jsonschema-generator" 7 | ) 8 | 9 | type EmbeddedType struct { 10 | Zoo string 11 | } 12 | 13 | type Item struct { 14 | Value string 15 | } 16 | 17 | type ExampleBasic struct { 18 | Foo bool `json:"foo"` 19 | Bar string `json:",omitempty"` 20 | Qux int8 21 | Baz []string 22 | EmbeddedType 23 | List []Item 24 | } 25 | 26 | func main() { 27 | s := &jsonschema.Document{} 28 | s.Read(&ExampleBasic{}) 29 | fmt.Println(s) 30 | } 31 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 2 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 3 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 4 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 5 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 6 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 7 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Máximo Cuadros 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-jsonschema-generator [![Build Status](https://img.shields.io/github/workflow/status/mcuadros/go-jsonschema-generator/Test.svg)](https://github.com/mcuadros/go-jsonschema-generator/actions) [![GoDoc](http://godoc.org/github.com/mcuadros/go-jsonschema-generator?status.png)](https://pkg.go.dev/github.com/mcuadros/go-jsonschema-generator) 2 | ============================== 3 | 4 | Basic [json-schema](http://json-schema.org/) generator based on Go types, for easy interchange of Go structures across languages. 5 | 6 | 7 | Installation 8 | ------------ 9 | 10 | The recommended way to install go-jsonschema-generator 11 | 12 | ``` 13 | go get github.com/mcuadros/go-jsonschema-generator 14 | ``` 15 | 16 | Examples 17 | -------- 18 | 19 | A basic example: 20 | 21 | ```go 22 | package main 23 | 24 | import ( 25 | "fmt" 26 | "github.com/mcuadros/go-jsonschema-generator" 27 | ) 28 | 29 | type EmbeddedType struct { 30 | Zoo string 31 | } 32 | 33 | type Item struct { 34 | Value string 35 | } 36 | 37 | type ExampleBasic struct { 38 | Foo bool `json:"foo"` 39 | Bar string `json:",omitempty"` 40 | Qux int8 41 | Baz []string 42 | EmbeddedType 43 | List []Item 44 | } 45 | 46 | func main() { 47 | s := &jsonschema.Document{} 48 | s.Read(&ExampleBasic{}) 49 | fmt.Println(s) 50 | } 51 | ``` 52 | 53 | ```json 54 | { 55 | "$schema": "http://json-schema.org/schema#", 56 | "type": "object", 57 | "properties": { 58 | "Bar": { 59 | "type": "string" 60 | }, 61 | "Baz": { 62 | "type": "array", 63 | "items": { 64 | "type": "string" 65 | } 66 | }, 67 | "List": { 68 | "type": "array", 69 | "items": { 70 | "type": "object", 71 | "properties": { 72 | "Value": { 73 | "type": "string" 74 | } 75 | }, 76 | "required": [ 77 | "Value" 78 | ] 79 | } 80 | }, 81 | "Qux": { 82 | "type": "integer" 83 | }, 84 | "Zoo": { 85 | "type": "string" 86 | }, 87 | "foo": { 88 | "type": "boolean" 89 | } 90 | }, 91 | "required": [ 92 | "foo", 93 | "Qux", 94 | "Baz", 95 | "Zoo", 96 | "List" 97 | ] 98 | } 99 | ``` 100 | 101 | License 102 | ------- 103 | 104 | MIT, see [LICENSE](LICENSE) 105 | -------------------------------------------------------------------------------- /jsonschema.go: -------------------------------------------------------------------------------- 1 | /* 2 | Basic json-schema generator based on Go types, for easy interchange of Go 3 | structures between diferent languages. 4 | */ 5 | package jsonschema 6 | 7 | import ( 8 | "encoding/json" 9 | "reflect" 10 | "strings" 11 | ) 12 | 13 | const DEFAULT_SCHEMA = "http://json-schema.org/schema#" 14 | 15 | type Document struct { 16 | Schema string `json:"$schema,omitempty"` 17 | property 18 | } 19 | 20 | // Reads the variable structure into the JSON-Schema Document 21 | func (d *Document) Read(variable interface{}) { 22 | d.setDefaultSchema() 23 | 24 | value := reflect.ValueOf(variable) 25 | d.read(value.Type(), tagOptions("")) 26 | } 27 | 28 | func (d *Document) setDefaultSchema() { 29 | if d.Schema == "" { 30 | d.Schema = DEFAULT_SCHEMA 31 | } 32 | } 33 | 34 | // Marshal returns the JSON encoding of the Document 35 | func (d *Document) Marshal() ([]byte, error) { 36 | return json.MarshalIndent(d, "", " ") 37 | } 38 | 39 | // String return the JSON encoding of the Document as a string 40 | func (d *Document) String() string { 41 | json, _ := d.Marshal() 42 | return string(json) 43 | } 44 | 45 | type property struct { 46 | Type string `json:"type,omitempty"` 47 | Format string `json:"format,omitempty"` 48 | Items *property `json:"items,omitempty"` 49 | Properties map[string]*property `json:"properties,omitempty"` 50 | Required []string `json:"required,omitempty"` 51 | AdditionalProperties bool `json:"additionalProperties,omitempty"` 52 | } 53 | 54 | func (p *property) read(t reflect.Type, opts tagOptions) { 55 | jsType, format, kind := getTypeFromMapping(t) 56 | if jsType != "" { 57 | p.Type = jsType 58 | } 59 | if format != "" { 60 | p.Format = format 61 | } 62 | 63 | switch kind { 64 | case reflect.Slice: 65 | p.readFromSlice(t) 66 | case reflect.Map: 67 | p.readFromMap(t) 68 | case reflect.Struct: 69 | p.readFromStruct(t) 70 | case reflect.Ptr: 71 | p.read(t.Elem(), opts) 72 | } 73 | } 74 | 75 | func (p *property) readFromSlice(t reflect.Type) { 76 | jsType, _, kind := getTypeFromMapping(t.Elem()) 77 | if kind == reflect.Uint8 { 78 | p.Type = "string" 79 | } else if jsType != "" { 80 | p.Items = &property{} 81 | p.Items.read(t.Elem(), tagOptions("")) 82 | } 83 | } 84 | 85 | func (p *property) readFromMap(t reflect.Type) { 86 | jsType, format, _ := getTypeFromMapping(t.Elem()) 87 | 88 | if jsType != "" { 89 | p.Properties = make(map[string]*property, 0) 90 | p.Properties[".*"] = &property{Type: jsType, Format: format} 91 | } else { 92 | p.AdditionalProperties = true 93 | } 94 | } 95 | 96 | func (p *property) readFromStruct(t reflect.Type) { 97 | p.Type = "object" 98 | p.Properties = make(map[string]*property, 0) 99 | p.AdditionalProperties = false 100 | 101 | count := t.NumField() 102 | for i := 0; i < count; i++ { 103 | field := t.Field(i) 104 | 105 | tag := field.Tag.Get("json") 106 | name, opts := parseTag(tag) 107 | if name == "" { 108 | name = field.Name 109 | } 110 | if name == "-" { 111 | continue 112 | } 113 | 114 | if field.Anonymous { 115 | embeddedProperty := &property{} 116 | embeddedProperty.read(field.Type, opts) 117 | 118 | for name, property := range embeddedProperty.Properties { 119 | p.Properties[name] = property 120 | } 121 | p.Required = append(p.Required, embeddedProperty.Required...) 122 | 123 | continue 124 | } 125 | 126 | p.Properties[name] = &property{} 127 | p.Properties[name].read(field.Type, opts) 128 | 129 | if !opts.Contains("omitempty") { 130 | p.Required = append(p.Required, name) 131 | } 132 | } 133 | } 134 | 135 | var formatMapping = map[string][]string{ 136 | "time.Time": []string{"string", "date-time"}, 137 | } 138 | 139 | var kindMapping = map[reflect.Kind]string{ 140 | reflect.Bool: "boolean", 141 | reflect.Int: "integer", 142 | reflect.Int8: "integer", 143 | reflect.Int16: "integer", 144 | reflect.Int32: "integer", 145 | reflect.Int64: "integer", 146 | reflect.Uint: "integer", 147 | reflect.Uint8: "integer", 148 | reflect.Uint16: "integer", 149 | reflect.Uint32: "integer", 150 | reflect.Uint64: "integer", 151 | reflect.Float32: "number", 152 | reflect.Float64: "number", 153 | reflect.String: "string", 154 | reflect.Slice: "array", 155 | reflect.Struct: "object", 156 | reflect.Map: "object", 157 | } 158 | 159 | func getTypeFromMapping(t reflect.Type) (string, string, reflect.Kind) { 160 | if v, ok := formatMapping[t.String()]; ok { 161 | return v[0], v[1], reflect.String 162 | } 163 | 164 | if v, ok := kindMapping[t.Kind()]; ok { 165 | return v, "", t.Kind() 166 | } 167 | 168 | return "", "", t.Kind() 169 | } 170 | 171 | type tagOptions string 172 | 173 | func parseTag(tag string) (string, tagOptions) { 174 | if idx := strings.Index(tag, ","); idx != -1 { 175 | return tag[:idx], tagOptions(tag[idx+1:]) 176 | } 177 | return tag, tagOptions("") 178 | } 179 | 180 | func (o tagOptions) Contains(optionName string) bool { 181 | if len(o) == 0 { 182 | return false 183 | } 184 | 185 | s := string(o) 186 | for s != "" { 187 | var next string 188 | i := strings.Index(s, ",") 189 | if i >= 0 { 190 | s, next = s[:i], s[i+1:] 191 | } 192 | if s == optionName { 193 | return true 194 | } 195 | s = next 196 | } 197 | return false 198 | } 199 | -------------------------------------------------------------------------------- /jsonschema_test.go: -------------------------------------------------------------------------------- 1 | package jsonschema 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | . "gopkg.in/check.v1" 8 | ) 9 | 10 | func Test(t *testing.T) { TestingT(t) } 11 | 12 | type propertySuite struct{} 13 | 14 | var _ = Suite(&propertySuite{}) 15 | 16 | type ExampleJSONBasic struct { 17 | Omitted string `json:"-,omitempty"` 18 | Bool bool `json:",omitempty"` 19 | Integer int `json:",omitempty"` 20 | Integer8 int8 `json:",omitempty"` 21 | Integer16 int16 `json:",omitempty"` 22 | Integer32 int32 `json:",omitempty"` 23 | Integer64 int64 `json:",omitempty"` 24 | UInteger uint `json:",omitempty"` 25 | UInteger8 uint8 `json:",omitempty"` 26 | UInteger16 uint16 `json:",omitempty"` 27 | UInteger32 uint32 `json:",omitempty"` 28 | UInteger64 uint64 `json:",omitempty"` 29 | String string `json:",omitempty"` 30 | Bytes []byte `json:",omitempty"` 31 | Float32 float32 `json:",omitempty"` 32 | Float64 float64 33 | Interface interface{} 34 | Timestamp time.Time `json:",omitempty"` 35 | } 36 | 37 | func (self *propertySuite) TestLoad(c *C) { 38 | j := &Document{} 39 | j.Read(&ExampleJSONBasic{}) 40 | 41 | c.Assert(*j, DeepEquals, Document{ 42 | Schema: "http://json-schema.org/schema#", 43 | property: property{ 44 | Type: "object", 45 | Required: []string{"Float64", "Interface"}, 46 | Properties: map[string]*property{ 47 | "Bool": &property{Type: "boolean"}, 48 | "Integer": &property{Type: "integer"}, 49 | "Integer8": &property{Type: "integer"}, 50 | "Integer16": &property{Type: "integer"}, 51 | "Integer32": &property{Type: "integer"}, 52 | "Integer64": &property{Type: "integer"}, 53 | "UInteger": &property{Type: "integer"}, 54 | "UInteger8": &property{Type: "integer"}, 55 | "UInteger16": &property{Type: "integer"}, 56 | "UInteger32": &property{Type: "integer"}, 57 | "UInteger64": &property{Type: "integer"}, 58 | "String": &property{Type: "string"}, 59 | "Bytes": &property{Type: "string"}, 60 | "Float32": &property{Type: "number"}, 61 | "Float64": &property{Type: "number"}, 62 | "Interface": &property{}, 63 | "Timestamp": &property{Type: "string", Format: "date-time"}, 64 | }, 65 | }, 66 | }) 67 | } 68 | 69 | type ExampleJSONBasicWithTag struct { 70 | Bool bool `json:"test"` 71 | } 72 | 73 | func (self *propertySuite) TestLoadWithTag(c *C) { 74 | j := &Document{} 75 | j.Read(&ExampleJSONBasicWithTag{}) 76 | 77 | c.Assert(*j, DeepEquals, Document{ 78 | Schema: "http://json-schema.org/schema#", 79 | property: property{ 80 | Type: "object", 81 | Required: []string{"test"}, 82 | Properties: map[string]*property{ 83 | "test": &property{Type: "boolean"}, 84 | }, 85 | }, 86 | }) 87 | } 88 | 89 | type SliceStruct struct { 90 | Value string 91 | } 92 | 93 | type ExampleJSONBasicSlices struct { 94 | Slice []string `json:",foo,omitempty"` 95 | SliceOfInterface []interface{} `json:",foo"` 96 | SliceOfStruct []SliceStruct 97 | } 98 | 99 | func (self *propertySuite) TestLoadSliceAndContains(c *C) { 100 | j := &Document{} 101 | j.Read(&ExampleJSONBasicSlices{}) 102 | 103 | c.Assert(*j, DeepEquals, Document{ 104 | Schema: "http://json-schema.org/schema#", 105 | property: property{ 106 | Type: "object", 107 | Properties: map[string]*property{ 108 | "Slice": &property{ 109 | Type: "array", 110 | Items: &property{Type: "string"}, 111 | }, 112 | "SliceOfInterface": &property{ 113 | Type: "array", 114 | }, 115 | "SliceOfStruct": &property{ 116 | Type: "array", 117 | Items: &property{ 118 | Type: "object", 119 | Required: []string{"Value"}, 120 | Properties: map[string]*property{ 121 | "Value": &property{ 122 | Type: "string", 123 | }, 124 | }, 125 | }, 126 | }, 127 | }, 128 | 129 | Required: []string{"SliceOfInterface", "SliceOfStruct"}, 130 | }, 131 | }) 132 | } 133 | 134 | type ExampleJSONNestedStruct struct { 135 | Struct struct { 136 | Foo string 137 | } 138 | } 139 | 140 | func (self *propertySuite) TestLoadNested(c *C) { 141 | j := &Document{} 142 | j.Read(&ExampleJSONNestedStruct{}) 143 | 144 | c.Assert(*j, DeepEquals, Document{ 145 | Schema: "http://json-schema.org/schema#", 146 | property: property{ 147 | Type: "object", 148 | Properties: map[string]*property{ 149 | "Struct": &property{ 150 | Type: "object", 151 | Properties: map[string]*property{ 152 | "Foo": &property{Type: "string"}, 153 | }, 154 | Required: []string{"Foo"}, 155 | }, 156 | }, 157 | Required: []string{"Struct"}, 158 | }, 159 | }) 160 | } 161 | 162 | type EmbeddedStruct struct { 163 | Foo string 164 | } 165 | 166 | type ExampleJSONEmbeddedStruct struct { 167 | EmbeddedStruct 168 | } 169 | 170 | func (self *propertySuite) TestLoadEmbedded(c *C) { 171 | j := &Document{} 172 | j.Read(&ExampleJSONEmbeddedStruct{}) 173 | 174 | c.Assert(*j, DeepEquals, Document{ 175 | Schema: "http://json-schema.org/schema#", 176 | property: property{ 177 | Type: "object", 178 | Properties: map[string]*property{ 179 | "Foo": &property{Type: "string"}, 180 | }, 181 | Required: []string{"Foo"}, 182 | }, 183 | }) 184 | } 185 | 186 | type ExampleJSONBasicMaps struct { 187 | Maps map[string]string `json:",omitempty"` 188 | MapOfInterface map[string]interface{} 189 | } 190 | 191 | func (self *propertySuite) TestLoadMap(c *C) { 192 | j := &Document{} 193 | j.Read(&ExampleJSONBasicMaps{}) 194 | 195 | c.Assert(*j, DeepEquals, Document{ 196 | Schema: "http://json-schema.org/schema#", 197 | property: property{ 198 | Type: "object", 199 | Properties: map[string]*property{ 200 | "Maps": &property{ 201 | Type: "object", 202 | Properties: map[string]*property{ 203 | ".*": &property{Type: "string"}, 204 | }, 205 | AdditionalProperties: false, 206 | }, 207 | "MapOfInterface": &property{ 208 | Type: "object", 209 | AdditionalProperties: true, 210 | }, 211 | }, 212 | Required: []string{"MapOfInterface"}, 213 | }, 214 | }) 215 | } 216 | 217 | func (self *propertySuite) TestLoadNonStruct(c *C) { 218 | j := &Document{} 219 | j.Read([]string{}) 220 | 221 | c.Assert(*j, DeepEquals, Document{ 222 | Schema: "http://json-schema.org/schema#", 223 | property: property{ 224 | Type: "array", 225 | Items: &property{Type: "string"}, 226 | }, 227 | }) 228 | } 229 | 230 | func (self *propertySuite) TestString(c *C) { 231 | j := &Document{} 232 | j.Read(true) 233 | 234 | expected := "{\n" + 235 | " \"$schema\": \"http://json-schema.org/schema#\",\n" + 236 | " \"type\": \"boolean\"\n" + 237 | "}" 238 | 239 | c.Assert(j.String(), Equals, expected) 240 | } 241 | 242 | func (self *propertySuite) TestMarshal(c *C) { 243 | j := &Document{} 244 | j.Read(10) 245 | 246 | expected := "{\n" + 247 | " \"$schema\": \"http://json-schema.org/schema#\",\n" + 248 | " \"type\": \"integer\"\n" + 249 | "}" 250 | 251 | json, err := j.Marshal() 252 | c.Assert(err, IsNil) 253 | c.Assert(string(json), Equals, expected) 254 | } 255 | --------------------------------------------------------------------------------