├── .gitattributes ├── cmd └── schema-generate │ ├── .gitignore │ ├── main_test.go │ ├── marshal_test.go │ └── main.go ├── .gitignore ├── .travis.yml ├── test ├── aprefnoprop.json ├── array.json ├── schemaid.json ├── simple.json ├── abandoned_test.go ├── recursion.json ├── issue14.json ├── customer.json ├── multiple.json ├── abandoned.json ├── issue39.json ├── anonarrayitems.json ├── example1.json ├── arrayofref.json ├── issue35.json ├── example1_test.go ├── example1a.json ├── nestedarrayofref.json ├── additionalProperties.json ├── additionalPropertiesMarshal.json ├── issue6.json ├── test.json ├── additionalProperties2.json ├── additionalProperties2_test.go └── additionalPropertiesMarshal_test.go ├── LICENSE.txt ├── Makefile ├── README.md ├── output_test.go ├── input.go ├── refresolver.go ├── jsonschemaparse_test.go ├── output.go ├── jsonschema.go ├── generator.go └── generator_test.go /.gitattributes: -------------------------------------------------------------------------------- 1 | output.go linguist-generated=false -------------------------------------------------------------------------------- /cmd/schema-generate/.gitignore: -------------------------------------------------------------------------------- 1 | schema-generate -------------------------------------------------------------------------------- /cmd/schema-generate/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /schema-generate 2 | .idea 3 | test/*_gen 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | script: 4 | - make codecheck 5 | - make test 6 | -------------------------------------------------------------------------------- /test/aprefnoprop.json: -------------------------------------------------------------------------------- 1 | { 2 | "definitions": { 3 | "apRefNoProp": { 4 | "additionalProperties": { 5 | "$ref": "#/definitions/thing" 6 | }, 7 | "type": "object" 8 | }, 9 | "thing": { 10 | "type": "object" 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /test/array.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "title": "ProductSet", 4 | "type": "object", 5 | "properties": { 6 | "the array": { 7 | "type": "array", 8 | "items": { 9 | "type": "integer" 10 | } 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /test/schemaid.json: -------------------------------------------------------------------------------- 1 | { 2 | "$id": "http://example.com/root.json", 3 | "definitions": { 4 | "A": { "$id": "#foo" }, 5 | "B": { 6 | "$id": "other.json", 7 | "definitions": { 8 | "X": { "$id": "#bar" }, 9 | "Y": { "$id": "t/inner.json" } 10 | } 11 | }, 12 | "C": { 13 | "$id": "urn:uuid:ee564b8a-7a87-4125-8c96-e9f123d6766f" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /test/simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "title": "Simple", 4 | "type": "object", 5 | "properties": { 6 | "name": { 7 | "type": "string" 8 | }, 9 | "subbie": { 10 | "type": "object", 11 | "title": "Not So Anonymous", 12 | "properties": { 13 | "age": { 14 | "type": "integer" 15 | } 16 | } 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /test/abandoned_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | "github.com/a-h/generate/test/abandoned_gen" 6 | ) 7 | 8 | func TestAbandoned(t *testing.T) { 9 | // this just tests the name generation works correctly 10 | r := abandoned.Root{ 11 | Name: "jonson", 12 | Abandoned: &abandoned.PackageList{}, 13 | } 14 | // the test is the presence of the Abandoned field 15 | if r.Abandoned == nil { 16 | t.Fatal("thats the test") 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/recursion.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-06/schema#", 3 | 4 | "definitions": { 5 | "person": { 6 | "type": "object", 7 | "properties": { 8 | "name": { "type": "string" }, 9 | "children": { 10 | "type": "array", 11 | "items": { "$ref": "#/definitions/person" }, 12 | "default": [] 13 | } 14 | } 15 | } 16 | }, 17 | 18 | "type": "object", 19 | 20 | "properties": { 21 | "person": { "$ref": "#/definitions/person" } 22 | } 23 | } -------------------------------------------------------------------------------- /test/issue14.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "Array without defined item", 4 | "type": "object", 5 | "additionalProperties": false, 6 | "required": [ 7 | "name" 8 | ], 9 | "properties": { 10 | "name": { 11 | "type": "string", 12 | "description": "Repository name." 13 | 14 | }, 15 | "repositories": { 16 | "type": "array", 17 | "description": "A set of additional repositories where packages can be found.", 18 | "additionalProperties": true 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/customer.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-06/schema#", 3 | "$id": "http://thing", 4 | 5 | "definitions": { 6 | "address": { 7 | "type": "object", 8 | "properties": { 9 | "street_address": { "type": "string" }, 10 | "city": { "type": "string" }, 11 | "state": { "type": "string" } 12 | }, 13 | "required": ["street_address", "city", "state"] 14 | } 15 | }, 16 | 17 | "type": "object", 18 | 19 | "properties": { 20 | "billing_address": { "$ref": "#/definitions/address" }, 21 | "shipping_address": { "$ref": "#/definitions/address" } 22 | } 23 | } -------------------------------------------------------------------------------- /test/multiple.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "title": "Multipass", 4 | "type": [ 5 | "array", 6 | "object" 7 | ], 8 | "items": { 9 | "type": "string" 10 | }, 11 | "properties": { 12 | "arr": { 13 | "$ref": "#/definitions/Thing" 14 | } 15 | }, 16 | "definitions": { 17 | "Thing": { 18 | "type": [ 19 | "array", 20 | "object" 21 | ], 22 | "items": { 23 | "type": "integer" 24 | }, 25 | "properties": { 26 | "age": { 27 | "type": ["integer", "string"] 28 | } 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/abandoned.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "name": "Repository Configuration", 4 | "type": "object", 5 | "additionalProperties": false, 6 | "required": [ 7 | "name" 8 | ], 9 | "properties": { 10 | "name": { 11 | "type": "string", 12 | "description": "Repository name." 13 | }, 14 | "abandoned": { 15 | "type": "object", 16 | "title": "Package List", 17 | "description": "List of packages marked as abandoned for this repository, the mark can be boolean or a package name/URL pointing to a recommended alternative." 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/issue39.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "title": "House", 4 | "type": "object", 5 | "definitions": { 6 | "address": { 7 | "type": "object", 8 | "properties": { 9 | "number": { "type": "integer" }, 10 | "postcode": { "type": "string" } 11 | } 12 | }, 13 | "owners": { 14 | "type": "array", 15 | "items": { 16 | "type": "object", 17 | "title": "person", 18 | "properties": { 19 | "name": { "type": "string" } 20 | } 21 | } 22 | } 23 | }, 24 | "properties": { 25 | "address": { "$ref": "#/definitions/address" }, 26 | "owners": { "$ref": "#/definitions/owners" } 27 | } 28 | } -------------------------------------------------------------------------------- /test/anonarrayitems.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "title": "AnonArray", 4 | "type": "object", 5 | "properties": { 6 | "arr": { 7 | "$ref": "#/definitions/TheArray" 8 | } 9 | }, 10 | "definitions": { 11 | "TheArray": { 12 | "type": "array", 13 | "items": { 14 | "type": "object", 15 | "properties": { 16 | "name": { 17 | "type": "string" 18 | }, 19 | "anon": { 20 | "type": "array", 21 | "items": { 22 | "type": "object", 23 | "properties": { 24 | "foo": { 25 | "type": "string" 26 | } 27 | } 28 | } 29 | } 30 | } 31 | } 32 | } 33 | 34 | } 35 | } -------------------------------------------------------------------------------- /test/example1.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "Product", 4 | "description": "A product from Acme's catalog", 5 | "type": "object", 6 | "properties": { 7 | "id": { 8 | "description": "The unique identifier for a product", 9 | "type": "integer" 10 | }, 11 | "name": { 12 | "description": "Name of the product", 13 | "type": "string" 14 | }, 15 | "price": { 16 | "type": "number", 17 | "minimum": 0, 18 | "exclusiveMinimum": true 19 | }, 20 | "tags": { 21 | "type": "array", 22 | "items": { 23 | "type": "string" 24 | }, 25 | "minItems": 1, 26 | "uniqueItems": true 27 | } 28 | }, 29 | "required": [ 30 | "id", 31 | "name", 32 | "price" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /test/arrayofref.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "title": "ProductSet", 4 | "type": "array", 5 | "items": { 6 | "$ref": "#/definitions/Product" 7 | }, 8 | "definitions": { 9 | "Product": { 10 | "title": "Product", 11 | "type": "object", 12 | "properties": { 13 | "id": { 14 | "description": "The unique identifier for a product", 15 | "type": "number" 16 | }, 17 | "name": { 18 | "type": "string" 19 | }, 20 | "price": { 21 | "type": "number", 22 | "minimum": 0, 23 | "exclusiveMinimum": true 24 | }, 25 | "tags": { 26 | "type": "array", 27 | "items": { 28 | "type": "string" 29 | }, 30 | "minItems": 1, 31 | "uniqueItems": true 32 | } 33 | }, 34 | "required": ["id", "name", "price"] 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /test/issue35.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | "title": "PingPong", 5 | "properties": { 6 | "pings": { 7 | "type": "array", 8 | "items": { 9 | "title": "Ping", 10 | "description": "Ping description", 11 | "properties": { 12 | "id": { 13 | "type": "string", 14 | "description": "ID" 15 | } 16 | } 17 | } 18 | }, 19 | "pongs": { 20 | "type": "array", 21 | "items": { 22 | "type": "object", 23 | "title": "Pong", 24 | "description": "Pong description", 25 | "properties": { 26 | "id": { 27 | "type": "string", 28 | "description": "ID" 29 | } 30 | } 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/example1_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | "github.com/a-h/generate/test/example1_gen" 7 | ) 8 | 9 | func TestExample1(t *testing.T) { 10 | params := []struct { 11 | Name string 12 | Data string 13 | ExpectedResult bool 14 | }{ 15 | { 16 | Name: "Blue Sky", 17 | Data: `{ 18 | "id": 1, 19 | "name": "Unbridled Optimism 2.0", 20 | "price": 99.99, 21 | "tags": [ "happy" ] }`, 22 | ExpectedResult: true, 23 | }, 24 | { 25 | Name: "Missing Price", 26 | Data: `{ 27 | "id": 1, 28 | "name": "Unbridled Optimism 2.0", 29 | "tags": [ "happy" ] }`, 30 | ExpectedResult: false, 31 | }, 32 | } 33 | 34 | for _, param := range params { 35 | 36 | prod := &example1.Product{} 37 | if err := json.Unmarshal([]byte(param.Data), &prod); err != nil { 38 | if param.ExpectedResult { 39 | t.Fatal(err) 40 | } 41 | } else { 42 | if !param.ExpectedResult { 43 | t.Fatal("Expected failure, got success: " + param.Name) 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Adrian Hesketh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PKG := . 2 | CMD := $(PKG)/cmd/schema-generate 3 | BIN := schema-generate 4 | 5 | # Build 6 | 7 | .PHONY: all clean 8 | 9 | all: clean $(BIN) 10 | 11 | $(BIN): generator.go jsonschema.go cmd/schema-generate/main.go 12 | @echo "+ Building $@" 13 | CGO_ENABLED="0" go build -v -o $@ $(CMD) 14 | 15 | clean: 16 | @echo "+ Cleaning $(PKG)" 17 | go clean -i $(PKG)/... 18 | rm -f $(BIN) 19 | rm -rf test/*_gen 20 | 21 | # Test 22 | 23 | # generate sources 24 | JSON := $(wildcard test/*.json) 25 | GENERATED_SOURCE := $(patsubst %.json,%_gen/generated.go,$(JSON)) 26 | test/%_gen/generated.go: test/%.json 27 | @echo "\n+ Generating code for $@" 28 | @D=$(shell echo $^ | sed 's/.json/_gen/'); \ 29 | [ ! -d $$D ] && mkdir -p $$D || true 30 | ./schema-generate -o $@ -p $(shell echo $^ | sed 's/test\///; s/.json//') $^ 31 | 32 | .PHONY: test codecheck fmt lint vet 33 | 34 | test: $(BIN) $(GENERATED_SOURCE) 35 | @echo "\n+ Executing tests for $(PKG)" 36 | go test -v -race -cover $(PKG)/... 37 | 38 | 39 | codecheck: fmt lint vet 40 | 41 | fmt: 42 | @echo "+ go fmt" 43 | go fmt $(PKG)/... 44 | 45 | lint: $(GOPATH)/bin/golint 46 | @echo "+ go lint" 47 | golint -min_confidence=0.1 $(PKG)/... 48 | 49 | $(GOPATH)/bin/golint: 50 | go get -v golang.org/x/lint/golint 51 | 52 | vet: 53 | @echo "+ go vet" 54 | go vet $(PKG)/... 55 | -------------------------------------------------------------------------------- /cmd/schema-generate/marshal_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | ) 7 | 8 | func TestThatJSONCanBeRoundtrippedUsingGeneratedStructs(t *testing.T) { 9 | j := `{"address":{"county":"countyValue"},"name":"nameValue"}` 10 | 11 | e := &Example{} 12 | err := json.Unmarshal([]byte(j), e) 13 | 14 | if err != nil { 15 | t.Fatal("Failed to unmarshall JSON with error ", err) 16 | } 17 | 18 | if e.Address.County != "countyValue" { 19 | t.Errorf("the county value was not found, expected 'countyValue' got '%s'", e.Address.County) 20 | } 21 | 22 | op, err := json.Marshal(e) 23 | 24 | if err != nil { 25 | t.Error("Failed to marshal JSON with error ", err) 26 | } 27 | 28 | if string(op) != j { 29 | t.Errorf("expected %s, got %s", j, string(op)) 30 | } 31 | } 32 | 33 | type Address struct { 34 | County string `json:"county,omitempty"` 35 | District string `json:"district,omitempty"` 36 | FlatNumber string `json:"flatNumber,omitempty"` 37 | HouseName string `json:"houseName,omitempty"` 38 | HouseNumber string `json:"houseNumber,omitempty"` 39 | Postcode string `json:"postcode,omitempty"` 40 | Street string `json:"street,omitempty"` 41 | Town string `json:"town,omitempty"` 42 | } 43 | 44 | type Example struct { 45 | Address *Address `json:"address,omitempty"` 46 | Name string `json:"name,omitempty"` 47 | Status *Status `json:"status,omitempty"` 48 | } 49 | 50 | type Status struct { 51 | Favouritecat string `json:"favouritecat,omitempty"` 52 | } 53 | -------------------------------------------------------------------------------- /test/example1a.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "Product set", 4 | "type": "array", 5 | "items": { 6 | "title": "Product", 7 | "type": "object", 8 | "properties": { 9 | "id": { 10 | "description": "The unique identifier for a product", 11 | "type": "number" 12 | }, 13 | "name": { 14 | "type": "string" 15 | }, 16 | "price": { 17 | "type": "number", 18 | "minimum": 0, 19 | "exclusiveMinimum": true 20 | }, 21 | "tags": { 22 | "type": "array", 23 | "items": { 24 | "type": "string" 25 | }, 26 | "minItems": 1, 27 | "uniqueItems": true 28 | }, 29 | "dimensions": { 30 | "type": "object", 31 | "properties": { 32 | "length": { 33 | "type": "number" 34 | }, 35 | "width": { 36 | "type": "number" 37 | }, 38 | "height": { 39 | "type": "number" 40 | } 41 | }, 42 | "required": [ 43 | "length", 44 | "width", 45 | "height" 46 | ] 47 | } 48 | }, 49 | "required": [ 50 | "id", 51 | "name", 52 | "price" 53 | ] 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/nestedarrayofref.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "title": "AccountTags", 4 | "comment": "goal: AccountTags [][]*Tags", 5 | "type": "array", 6 | "items": { 7 | "$ref": "#/definitions/Accounts" 8 | }, 9 | "definitions": { 10 | "Accounts": { 11 | "title": "Accounts", 12 | "type": "array", 13 | "items": { 14 | "$ref": "#/definitions/Tags" 15 | } 16 | }, 17 | "Tags": { 18 | "title": "Tags", 19 | "type": "object", 20 | "properties": { 21 | "tag": { 22 | "type": "string" 23 | }, 24 | "things": { 25 | "type": "array", 26 | "items": { 27 | "$ref": "#/definitions/Thing" 28 | } 29 | }, 30 | "mainThing": { 31 | "$ref": "#/definitions/Thing" 32 | } 33 | } 34 | }, 35 | "Thing": { 36 | "type": "object", 37 | "properties": { 38 | "foo": { 39 | "type": "object", 40 | "properties": { 41 | "name": { 42 | "type": "string" 43 | }, 44 | "age": { 45 | "type": "integer" 46 | } 47 | } 48 | }, 49 | "bar": { 50 | "type": "array", 51 | "items": { 52 | "type": "integer" 53 | } 54 | }, 55 | "baz": { 56 | "type": "array", 57 | "items": { 58 | "type": "object", 59 | "properties": { 60 | "qux": { 61 | "type": "string" 62 | } 63 | } 64 | } 65 | } 66 | } 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /test/additionalProperties.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "title": "additional properties", 4 | "type": "object", 5 | "properties": { 6 | "property1": { 7 | "type": "string" 8 | }, 9 | "property2": { 10 | "$ref": "#/definitions/address" 11 | }, 12 | "property3": { 13 | "type": "object", 14 | "additionalProperties": { 15 | "type": "integer" 16 | } 17 | }, 18 | "property4": { 19 | "type": "object", 20 | "additionalProperties": { 21 | "type": "string" 22 | } 23 | }, 24 | "property5": { 25 | "type": "object", 26 | "additionalProperties": { 27 | "type": "object", 28 | "title": "not so anonymous", 29 | "properties": { 30 | "subproperty1": { 31 | "type": "integer" 32 | } 33 | } 34 | } 35 | }, 36 | "property6": { 37 | "type": "object", 38 | "additionalProperties": { 39 | "type": "object", 40 | "properties": { 41 | "subproperty1": { 42 | "type": "integer" 43 | } 44 | } 45 | } 46 | }, 47 | "property7": { 48 | "type": "object", 49 | "additionalProperties": { 50 | "type": "object", 51 | "title": "hairy", 52 | "additionalProperties": { 53 | "type": "object", 54 | "properties": { 55 | "subproperty1": { 56 | "type": "integer" 57 | } 58 | } 59 | } 60 | } 61 | } 62 | }, 63 | "definitions": { 64 | "address": { 65 | "type": "object", 66 | "properties": { 67 | "city": { 68 | "type": "string" 69 | } 70 | } 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /cmd/schema-generate/main.go: -------------------------------------------------------------------------------- 1 | // The schema-generate binary reads the JSON schema files passed as arguments 2 | // and outputs the corresponding Go structs. 3 | package main 4 | 5 | import ( 6 | "flag" 7 | "fmt" 8 | "io" 9 | "os" 10 | 11 | "github.com/a-h/generate" 12 | ) 13 | 14 | var ( 15 | o = flag.String("o", "", "The output file for the schema.") 16 | p = flag.String("p", "main", "The package that the structs are created in.") 17 | i = flag.String("i", "", "A single file path (used for backwards compatibility).") 18 | schemaKeyRequiredFlag = flag.Bool("schemaKeyRequired", false, "Allow input files with no $schema key.") 19 | ) 20 | 21 | func main() { 22 | flag.Usage = func() { 23 | fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) 24 | flag.PrintDefaults() 25 | fmt.Fprintln(os.Stderr, " paths") 26 | fmt.Fprintln(os.Stderr, "\tThe input JSON Schema files.") 27 | } 28 | 29 | flag.Parse() 30 | 31 | inputFiles := flag.Args() 32 | if *i != "" { 33 | inputFiles = append(inputFiles, *i) 34 | } 35 | if len(inputFiles) == 0 { 36 | fmt.Fprintln(os.Stderr, "No input JSON Schema files.") 37 | flag.Usage() 38 | os.Exit(1) 39 | } 40 | 41 | schemas, err := generate.ReadInputFiles(inputFiles, *schemaKeyRequiredFlag) 42 | if err != nil { 43 | fmt.Fprintf(os.Stderr, err.Error()) 44 | os.Exit(1) 45 | } 46 | 47 | g := generate.New(schemas...) 48 | 49 | err = g.CreateTypes() 50 | if err != nil { 51 | fmt.Fprintln(os.Stderr, "Failure generating structs: ", err) 52 | os.Exit(1) 53 | } 54 | 55 | var w io.Writer = os.Stdout 56 | 57 | if *o != "" { 58 | w, err = os.Create(*o) 59 | 60 | if err != nil { 61 | fmt.Fprintln(os.Stderr, "Error opening output file: ", err) 62 | return 63 | } 64 | } 65 | 66 | generate.Output(w, g, *p) 67 | } 68 | -------------------------------------------------------------------------------- /test/additionalPropertiesMarshal.json: -------------------------------------------------------------------------------- 1 | { 2 | "definitions": { 3 | "thing": { 4 | "type": "object" 5 | }, 6 | 7 | "apRefNoProp": { 8 | "additionalProperties": { 9 | "$ref": "#/definitions/thing" 10 | }, 11 | "type": "object" 12 | }, 13 | "apRefProp": { 14 | "properties": { 15 | "stuff": { 16 | "type": "string" 17 | } 18 | }, 19 | "additionalProperties": { 20 | "$ref": "#/definitions/thing" 21 | }, 22 | "type": "object" 23 | }, 24 | "apRefReqProp": { 25 | "properties": { 26 | "stuff": { 27 | "type": "string" 28 | } 29 | }, 30 | "additionalProperties": { 31 | "$ref": "#/definitions/thing" 32 | }, 33 | "required": [ "stuff" ], 34 | "type": "object" 35 | }, 36 | 37 | 38 | "apTrueNoProp": { 39 | "additionalProperties": true, 40 | "type": "object" 41 | }, 42 | "apFalseNoProp": { 43 | "additionalProperties": false, 44 | "type": "object" 45 | }, 46 | 47 | 48 | "apTrueProp": { 49 | "properties": { 50 | "stuff": { 51 | "type": "string" 52 | } 53 | }, 54 | "additionalProperties": true, 55 | "type": "object" 56 | }, 57 | "apFalseProp": { 58 | "properties": { 59 | "stuff": { 60 | "type": "string" 61 | } 62 | }, 63 | "additionalProperties": false, 64 | "type": "object" 65 | }, 66 | 67 | 68 | "apTrueReqProp": { 69 | "properties": { 70 | "stuff": { 71 | "type": "string" 72 | } 73 | }, 74 | "additionalProperties": true, 75 | "required": [ "stuff" ], 76 | "type": "object" 77 | }, 78 | "apFalseReqProp": { 79 | "properties": { 80 | "stuff": { 81 | "type": "string" 82 | } 83 | }, 84 | "additionalProperties": false, 85 | "required": [ "stuff" ], 86 | "type": "object" 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /test/issue6.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "$id": "file://physical_server.json", 4 | "description": "Physical Server asset", 5 | "type": "object", 6 | "properties": { 7 | "attributes": { 8 | "type": "object", 9 | "description": "Specific attributes of the asset", 10 | "properties": { 11 | "hostname": { 12 | "type": "string" 13 | }, 14 | "operating_system": { 15 | "type": "object", 16 | "description": "Running operating system", 17 | "properties": { 18 | "family": { 19 | "type": "string" 20 | }, 21 | "name": { 22 | "type": "string" 23 | }, 24 | "version": { 25 | "type": "string" 26 | }, 27 | "revision": { 28 | "type": "string" 29 | } 30 | } 31 | }, 32 | "location": { 33 | "type": "string", 34 | "description": "Physical location of this server (datacenter, rack, pod, etc)" 35 | }, 36 | "serial_number": { 37 | "type": "string" 38 | } 39 | } 40 | }, 41 | "links": { 42 | "type": "array", 43 | "items": { 44 | "type": "object", 45 | "properties": { 46 | "asset_id": { 47 | "type": "string" 48 | }, 49 | "relationship": { 50 | "type": "string" 51 | }, 52 | "description": { 53 | "type": "string" 54 | } 55 | } 56 | } 57 | }, 58 | "tags": { 59 | "type": "array", 60 | "items": { 61 | "type": "string" 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # generate 2 | 3 | Generates Go (golang) Structs and Validation code from JSON schema. 4 | 5 | # Requirements 6 | 7 | * Go 1.8+ 8 | 9 | # Usage 10 | 11 | Install 12 | 13 | ```console 14 | $ go get -u github.com/a-h/generate/... 15 | ``` 16 | 17 | or 18 | 19 | Build 20 | 21 | ```console 22 | $ make 23 | ``` 24 | 25 | Run 26 | 27 | ```console 28 | $ schema-generate exampleschema.json 29 | ``` 30 | 31 | # Example 32 | 33 | This schema 34 | 35 | ```json 36 | { 37 | "$schema": "http://json-schema.org/draft-04/schema#", 38 | "title": "Example", 39 | "id": "http://example.com/exampleschema.json", 40 | "type": "object", 41 | "description": "An example JSON Schema", 42 | "properties": { 43 | "name": { 44 | "type": "string" 45 | }, 46 | "address": { 47 | "$ref": "#/definitions/address" 48 | }, 49 | "status": { 50 | "$ref": "#/definitions/status" 51 | } 52 | }, 53 | "definitions": { 54 | "address": { 55 | "id": "address", 56 | "type": "object", 57 | "description": "Address", 58 | "properties": { 59 | "street": { 60 | "type": "string", 61 | "description": "Address 1", 62 | "maxLength": 40 63 | }, 64 | "houseNumber": { 65 | "type": "integer", 66 | "description": "House Number" 67 | } 68 | } 69 | }, 70 | "status": { 71 | "type": "object", 72 | "properties": { 73 | "favouritecat": { 74 | "enum": [ 75 | "A", 76 | "B", 77 | "C" 78 | ], 79 | "type": "string", 80 | "description": "The favourite cat.", 81 | "maxLength": 1 82 | } 83 | } 84 | } 85 | } 86 | } 87 | ``` 88 | 89 | generates 90 | 91 | ```go 92 | package main 93 | 94 | type Address struct { 95 | HouseNumber int `json:"houseNumber,omitempty"` 96 | Street string `json:"street,omitempty"` 97 | } 98 | 99 | type Example struct { 100 | Address *Address `json:"address,omitempty"` 101 | Name string `json:"name,omitempty"` 102 | Status *Status `json:"status,omitempty"` 103 | } 104 | 105 | type Status struct { 106 | Favouritecat string `json:"favouritecat,omitempty"` 107 | } 108 | ``` 109 | 110 | See the [test/](./test/) directory for more examples. 111 | -------------------------------------------------------------------------------- /output_test.go: -------------------------------------------------------------------------------- 1 | package generate 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestThatFieldNamesAreOrdered(t *testing.T) { 10 | m := map[string]Field{ 11 | "z": {}, 12 | "b": {}, 13 | } 14 | 15 | actual := getOrderedFieldNames(m) 16 | expected := []string{"b", "z"} 17 | 18 | if !reflect.DeepEqual(actual, expected) { 19 | t.Errorf("expected %s and actual %s should match in order", strings.Join(expected, ", "), strings.Join(actual, ",")) 20 | } 21 | } 22 | 23 | func TestThatStructNamesAreOrdered(t *testing.T) { 24 | m := map[string]Struct{ 25 | "c": {}, 26 | "b": {}, 27 | "a": {}, 28 | } 29 | 30 | actual := getOrderedStructNames(m) 31 | expected := []string{"a", "b", "c"} 32 | 33 | if !reflect.DeepEqual(actual, expected) { 34 | t.Errorf("expected %s and actual %s should match in order", strings.Join(expected, ", "), strings.Join(actual, ",")) 35 | } 36 | } 37 | 38 | func TestLineAndCharacterFromOffset(t *testing.T) { 39 | tests := []struct { 40 | In []byte 41 | Offset int 42 | ExpectedLine int 43 | ExpectedCharacter int 44 | ExpectedError bool 45 | }{ 46 | { 47 | In: []byte("Line 1\nLine 2"), 48 | Offset: 6, 49 | ExpectedLine: 2, 50 | ExpectedCharacter: 1, 51 | }, 52 | { 53 | In: []byte("Line 1\r\nLine 2"), 54 | Offset: 7, 55 | ExpectedLine: 2, 56 | ExpectedCharacter: 1, 57 | }, 58 | { 59 | In: []byte("Line 1\nLine 2"), 60 | Offset: 0, 61 | ExpectedLine: 1, 62 | ExpectedCharacter: 1, 63 | }, 64 | { 65 | In: []byte("Line 1\nLine 2"), 66 | Offset: 200, 67 | ExpectedLine: 0, 68 | ExpectedCharacter: 0, 69 | ExpectedError: true, 70 | }, 71 | { 72 | In: []byte("Line 1\nLine 2"), 73 | Offset: -1, 74 | ExpectedLine: 0, 75 | ExpectedCharacter: 0, 76 | ExpectedError: true, 77 | }, 78 | } 79 | 80 | for _, test := range tests { 81 | actualLine, actualCharacter, err := lineAndCharacter(test.In, test.Offset) 82 | if err != nil && !test.ExpectedError { 83 | t.Errorf("Unexpected error for input %s at offset %d: %v", test.In, test.Offset, err) 84 | continue 85 | } 86 | 87 | if actualLine != test.ExpectedLine || actualCharacter != test.ExpectedCharacter { 88 | t.Errorf("For '%s' at offset %d, expected %d:%d, but got %d:%d", test.In, test.Offset, test.ExpectedLine, test.ExpectedCharacter, actualLine, actualCharacter) 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /test/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "Example", 4 | "$id": "http://Example", 5 | "type": "object", 6 | "description": "example", 7 | "definitions": { 8 | "address": { 9 | "$id": "address", 10 | "type": "object", 11 | "description": "Address", 12 | "properties": { 13 | "houseName": { 14 | "type": "string", 15 | "description": "House Name", 16 | "maxLength": 30 17 | }, 18 | "houseNumber": { 19 | "type": "string", 20 | "description": "House Number", 21 | "maxLength": 4 22 | }, 23 | "flatNumber": { 24 | "type": "string", 25 | "description": "Flat", 26 | "maxLength": 15 27 | }, 28 | "street": { 29 | "type": "string", 30 | "description": "Address 1", 31 | "maxLength": 40 32 | }, 33 | "district": { 34 | "type": "string", 35 | "description": "Address 2", 36 | "maxLength": 30 37 | }, 38 | "town": { 39 | "type": "string", 40 | "description": "City", 41 | "maxLength": 20 42 | }, 43 | "county": { 44 | "type": "string", 45 | "description": "County", 46 | "maxLength": 20 47 | }, 48 | "postcode": { 49 | "type": "string", 50 | "description": "Postcode", 51 | "maxLength": 8 52 | } 53 | } 54 | }, 55 | "status": { 56 | "type": "object", 57 | "properties": { 58 | "favouritecat": { 59 | "enum": [ 60 | "A", 61 | "B", 62 | "C", 63 | "D", 64 | "E", 65 | "F" 66 | ], 67 | "type": "string", 68 | "description": "The favourite cat.", 69 | "maxLength": 1 70 | } 71 | } 72 | } 73 | }, 74 | "properties": { 75 | "name": { 76 | "type": "string" 77 | }, 78 | "address": { 79 | "$ref": "#/definitions/address" 80 | }, 81 | "status": { 82 | "$ref": "#/definitions/status" 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /input.go: -------------------------------------------------------------------------------- 1 | package generate 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "net/url" 9 | "os" 10 | "path" 11 | ) 12 | 13 | // ReadInputFiles from disk and convert to JSON schema. 14 | func ReadInputFiles(inputFiles []string, schemaKeyRequired bool) ([]*Schema, error) { 15 | schemas := make([]*Schema, len(inputFiles)) 16 | for i, file := range inputFiles { 17 | b, err := ioutil.ReadFile(file) 18 | if err != nil { 19 | return nil, errors.New("failed to read the input file with error " + err.Error()) 20 | } 21 | 22 | abPath, err := abs(file) 23 | if err != nil { 24 | return nil, errors.New("failed to normalise input path with error " + err.Error()) 25 | } 26 | 27 | fileURI := url.URL{ 28 | Scheme: "file", 29 | Path: abPath, 30 | } 31 | 32 | schemas[i], err = ParseWithSchemaKeyRequired(string(b), &fileURI, schemaKeyRequired) 33 | if err != nil { 34 | if jsonError, ok := err.(*json.SyntaxError); ok { 35 | line, character, lcErr := lineAndCharacter(b, int(jsonError.Offset)) 36 | errStr := fmt.Sprintf("cannot parse JSON schema due to a syntax error at %s line %d, character %d: %v\n", file, line, character, jsonError.Error()) 37 | if lcErr != nil { 38 | errStr += fmt.Sprintf("couldn't find the line and character position of the error due to error %v\n", lcErr) 39 | } 40 | return nil, errors.New(errStr) 41 | } 42 | if jsonError, ok := err.(*json.UnmarshalTypeError); ok { 43 | line, character, lcErr := lineAndCharacter(b, int(jsonError.Offset)) 44 | errStr := fmt.Sprintf("the JSON type '%v' cannot be converted into the Go '%v' type on struct '%s', field '%v'. See input file %s line %d, character %d\n", jsonError.Value, jsonError.Type.Name(), jsonError.Struct, jsonError.Field, file, line, character) 45 | if lcErr != nil { 46 | errStr += fmt.Sprintf("couldn't find the line and character position of the error due to error %v\n", lcErr) 47 | } 48 | return nil, errors.New(errStr) 49 | } 50 | return nil, fmt.Errorf("failed to parse the input JSON schema file %s with error %v", file, err) 51 | } 52 | } 53 | 54 | return schemas, nil 55 | } 56 | 57 | func lineAndCharacter(bytes []byte, offset int) (line int, character int, err error) { 58 | lf := byte(0x0A) 59 | 60 | if offset > len(bytes) { 61 | return 0, 0, fmt.Errorf("couldn't find offset %d in %d bytes", offset, len(bytes)) 62 | } 63 | 64 | // Humans tend to count from 1. 65 | line = 1 66 | 67 | for i, b := range bytes { 68 | if b == lf { 69 | line++ 70 | character = 0 71 | } 72 | character++ 73 | if i == offset { 74 | return line, character, nil 75 | } 76 | } 77 | 78 | return 0, 0, fmt.Errorf("couldn't find offset %d in %d bytes", offset, len(bytes)) 79 | } 80 | 81 | func abs(name string) (string, error) { 82 | if path.IsAbs(name) { 83 | return name, nil 84 | } 85 | wd, err := os.Getwd() 86 | return path.Join(wd, name), err 87 | } 88 | -------------------------------------------------------------------------------- /test/additionalProperties2.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "title": "additional properties", 4 | "type": "object", 5 | "properties": { 6 | "property1": { 7 | "type": "string" 8 | }, 9 | "property2": { 10 | "$ref": "#/definitions/address" 11 | }, 12 | "property3": { 13 | "type": "object", 14 | "properties": { 15 | "age": { "type": "integer"} 16 | }, 17 | "additionalProperties": { 18 | "type": "integer" 19 | } 20 | }, 21 | "property4": { 22 | "type": "object", 23 | "properties": { 24 | "age": { "type": "integer"} 25 | }, 26 | "additionalProperties": { 27 | "type": "string" 28 | } 29 | }, 30 | "property5": { 31 | "type": "object", 32 | "properties": { 33 | "age": { "type": "integer"} 34 | }, 35 | "additionalProperties": { 36 | "type": "object", 37 | "title": "not so anonymous", 38 | "properties": { 39 | "subproperty1": { 40 | "type": "integer" 41 | } 42 | 43 | } 44 | } 45 | }, 46 | "property6": { 47 | "type": "object", 48 | "properties": { 49 | "age": { "type": "integer" }, 50 | "pronoun": { "type": "string" } 51 | }, 52 | "additionalProperties": { 53 | "type": "object", 54 | "properties": { 55 | "subproperty1": { 56 | "type": "integer" 57 | } 58 | } 59 | } 60 | }, 61 | "property7": { 62 | "type": "object", 63 | "properties": { 64 | "street_number": { "type": "integer"}, 65 | "street_name": { "type": "string"}, 66 | "po_box": { 67 | "type": "object", 68 | "properties": { 69 | "suburb": { 70 | "type": "string" 71 | } 72 | } 73 | } 74 | }, 75 | "additionalProperties": { 76 | "type": "object", 77 | "title": "hairy", 78 | "additionalProperties": { 79 | "type": "object", 80 | "properties": { 81 | "color": { 82 | "type": "string" 83 | }, 84 | "density": { 85 | "type": "number" 86 | }, 87 | "conditions": { 88 | "type": "array", 89 | "items": { 90 | "type": "object", 91 | "properties": { 92 | "name": { 93 | "type": "string" 94 | } 95 | } 96 | } 97 | } 98 | } 99 | } 100 | } 101 | } 102 | }, 103 | "definitions": { 104 | "address": { 105 | "type": "object", 106 | "properties": { 107 | "city": { 108 | "type": "string" 109 | } 110 | } 111 | } 112 | } 113 | } -------------------------------------------------------------------------------- /test/additionalProperties2_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/a-h/generate/test/additionalProperties2_gen" 10 | ) 11 | 12 | func TestMarshalUnmarshal(t *testing.T) { 13 | params := []struct { 14 | Name string 15 | Strct additionalProperties2.AdditionalProperties 16 | Validation func(t *testing.T, prop *additionalProperties2.AdditionalProperties) 17 | }{ 18 | { 19 | Name: "Base Object", 20 | Strct: additionalProperties2.AdditionalProperties{ 21 | Property1: "test", 22 | }, 23 | Validation: func(t *testing.T, prop *additionalProperties2.AdditionalProperties) { 24 | if prop.Property1 != "test" { 25 | t.Fatal("property1 != test") 26 | } 27 | }, 28 | }, 29 | { 30 | Name: "Property7", 31 | Strct: additionalProperties2.AdditionalProperties{ 32 | Property7: &additionalProperties2.Property7{ 33 | StreetNumber: 69, 34 | StreetName: "Elm St", 35 | PoBox: &additionalProperties2.PoBox{ 36 | Suburb: "Smallville", 37 | }, 38 | AdditionalProperties: map[string]map[string]*additionalProperties2.Anonymous1{ 39 | "red": { 40 | "blue": { 41 | Color: "green", 42 | Conditions: []*additionalProperties2.ConditionsItems{ 43 | {Name: "dry"}, 44 | }, 45 | Density: 42.42, 46 | }, 47 | }, 48 | "orange": {}, 49 | }, 50 | }, 51 | }, 52 | Validation: func(t *testing.T, prop *additionalProperties2.AdditionalProperties) { 53 | 54 | if prop.Property7.StreetNumber != 69 { 55 | t.Fatal("wrong value") 56 | } 57 | 58 | if len(prop.Property7.AdditionalProperties) != 2 { 59 | t.Fatal("not enough additionalProperties") 60 | } 61 | 62 | if prop.Property7.AdditionalProperties["red"]["blue"].Color != "green" { 63 | t.Fatal("wrong nested value") 64 | } 65 | 66 | if prop.Property7.AdditionalProperties["red"]["blue"].Density != 42.42 { 67 | t.Fatal("wrong nested value") 68 | } 69 | }, 70 | }, 71 | } 72 | 73 | for _, p := range params { 74 | if str, err := json.MarshalIndent(&p.Strct, "", " "); err != nil { 75 | t.Fatal(err) 76 | } else { 77 | //log.Println(string(str)) 78 | strct2 := &additionalProperties2.AdditionalProperties{} 79 | if err := json.Unmarshal(str, &strct2); err != nil { 80 | t.Fatal(err) 81 | } 82 | 83 | if reflect.DeepEqual(p.Strct, strct2) { 84 | log.Fatal("unmarshaled struct != given struct") 85 | } 86 | 87 | p.Validation(t, strct2) 88 | 89 | if str, err := json.MarshalIndent(&strct2, "", " "); err != nil { 90 | t.Fatal(err) 91 | } else { 92 | //log.Println(string(str)) 93 | strct3 := &additionalProperties2.AdditionalProperties{} 94 | if err := json.Unmarshal(str, &strct3); err != nil { 95 | t.Fatal(err) 96 | } 97 | 98 | if reflect.DeepEqual(p.Strct, strct3) { 99 | log.Fatal("unmarshaled struct != given struct") 100 | } 101 | 102 | p.Validation(t, strct3) 103 | 104 | } 105 | 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /refresolver.go: -------------------------------------------------------------------------------- 1 | package generate 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/url" 7 | "strings" 8 | ) 9 | 10 | // RefResolver allows references to be resolved. 11 | type RefResolver struct { 12 | schemas []*Schema 13 | // k=uri v=Schema 14 | pathToSchema map[string]*Schema 15 | } 16 | 17 | // NewRefResolver creates a reference resolver. 18 | func NewRefResolver(schemas []*Schema) *RefResolver { 19 | return &RefResolver{ 20 | schemas: schemas, 21 | } 22 | } 23 | 24 | // Init the resolver. 25 | func (r *RefResolver) Init() error { 26 | r.pathToSchema = make(map[string]*Schema) 27 | for _, v := range r.schemas { 28 | if err := r.mapPaths(v); err != nil { 29 | return err 30 | } 31 | } 32 | return nil 33 | } 34 | 35 | // recusively generate path to schema 36 | func getPath(schema *Schema, path string) string { 37 | path = schema.PathElement + "/" + path 38 | if schema.IsRoot() { 39 | return path 40 | } 41 | return getPath(schema.Parent, path) 42 | } 43 | 44 | // GetPath generates a path to given schema. 45 | func (r *RefResolver) GetPath(schema *Schema) string { 46 | if schema.IsRoot() { 47 | return "#" 48 | } 49 | return getPath(schema.Parent, schema.PathElement) 50 | } 51 | 52 | // GetSchemaByReference returns the schema. 53 | func (r *RefResolver) GetSchemaByReference(schema *Schema) (*Schema, error) { 54 | u, err := url.Parse(schema.GetRoot().ID()) 55 | if err != nil { 56 | return nil, err 57 | } 58 | ref, err := url.Parse(schema.Reference) 59 | if err != nil { 60 | return nil, err 61 | } 62 | resolvedPath := u.ResolveReference(ref) 63 | path, ok := r.pathToSchema[resolvedPath.String()] 64 | if !ok { 65 | return nil, errors.New("refresolver.GetSchemaByReference: reference not found: " + schema.Reference) 66 | } 67 | return path, nil 68 | } 69 | 70 | func (r *RefResolver) mapPaths(schema *Schema) error { 71 | rootURI := &url.URL{} 72 | id := schema.ID() 73 | if id == "" { 74 | if err := r.InsertURI("#", schema); err != nil { 75 | return err 76 | } 77 | } else { 78 | var err error 79 | rootURI, err = url.Parse(id) 80 | if err != nil { 81 | return err 82 | } 83 | // ensure no fragment. 84 | rootURI.Fragment = "" 85 | if err := r.InsertURI(rootURI.String(), schema); err != nil { 86 | return err 87 | } 88 | // add as JSON pointer (?) 89 | if err := r.InsertURI(rootURI.String()+"#", schema); err != nil { 90 | return err 91 | } 92 | } 93 | r.updateURIs(schema, *rootURI, false, false) 94 | return nil 95 | } 96 | 97 | // create a map of base URIs 98 | func (r *RefResolver) updateURIs(schema *Schema, baseURI url.URL, checkCurrentID bool, ignoreFragments bool) error { 99 | // already done for root, and if schema sets a new base URI 100 | if checkCurrentID { 101 | id := schema.ID() 102 | if id != "" { 103 | newBase, err := url.Parse(id) 104 | if err != nil { 105 | return err 106 | } 107 | // if it's a JSON fragment and we're coming from part of the tree where the baseURI has changed, we need to 108 | // ignore the fragment, since it won't be resolvable under the current baseURI. 109 | if !(strings.HasPrefix(id, "#") && ignoreFragments) { 110 | // map all the subschema under the new base 111 | resolved := baseURI.ResolveReference(newBase) 112 | if err := r.InsertURI(resolved.String(), schema); err != nil { 113 | return err 114 | } 115 | if resolved.Fragment == "" { 116 | if err := r.InsertURI(resolved.String()+"#", schema); err != nil { 117 | return err 118 | } 119 | } 120 | if err := r.updateURIs(schema, *resolved, false, false); err != nil { 121 | return err 122 | } 123 | // and continue to map all subschema under the old base (except for fragments) 124 | ignoreFragments = true 125 | } 126 | } 127 | } 128 | for k, subSchema := range schema.Definitions { 129 | newBaseURI := baseURI 130 | newBaseURI.Fragment += "/definitions/" + k 131 | if err := r.InsertURI(newBaseURI.String(), subSchema); err != nil { 132 | return err 133 | } 134 | r.updateURIs(subSchema, newBaseURI, true, ignoreFragments) 135 | } 136 | for k, subSchema := range schema.Properties { 137 | newBaseURI := baseURI 138 | newBaseURI.Fragment += "/properties/" + k 139 | if err := r.InsertURI(newBaseURI.String(), subSchema); err != nil { 140 | return err 141 | } 142 | r.updateURIs(subSchema, newBaseURI, true, ignoreFragments) 143 | } 144 | if schema.AdditionalProperties != nil { 145 | newBaseURI := baseURI 146 | newBaseURI.Fragment += "/additionalProperties" 147 | r.updateURIs((*Schema)(schema.AdditionalProperties), newBaseURI, true, ignoreFragments) 148 | } 149 | if schema.Items != nil { 150 | newBaseURI := baseURI 151 | newBaseURI.Fragment += "/items" 152 | r.updateURIs(schema.Items, newBaseURI, true, ignoreFragments) 153 | } 154 | return nil 155 | } 156 | 157 | // InsertURI to the references. 158 | func (r *RefResolver) InsertURI(uri string, schema *Schema) error { 159 | if _, ok := r.pathToSchema[uri]; ok { 160 | return fmt.Errorf("attempted to add duplicate uri: %s/%s", schema.GetRoot().ID(), uri) 161 | } 162 | r.pathToSchema[uri] = schema 163 | return nil 164 | } 165 | -------------------------------------------------------------------------------- /jsonschemaparse_test.go: -------------------------------------------------------------------------------- 1 | package generate 2 | 3 | import ( 4 | "net/url" 5 | "testing" 6 | ) 7 | 8 | func TestThatAMissingSchemaKeyResultsInAnError(t *testing.T) { 9 | invalid := `{ 10 | "title": "root" 11 | }` 12 | _, invaliderr := Parse(invalid, &url.URL{Scheme: "file", Path: "jsonschemaparse_test.go"}) 13 | valid := `{ 14 | "$schema": "http://json-schema.org/schema#", 15 | "title": "root" 16 | }` 17 | _, validerr := Parse(valid, &url.URL{Scheme: "file", Path: "jsonschemaparse_test.go"}) 18 | if invaliderr == nil { 19 | // it SHOULD be used in the root schema 20 | // t.Error("When the $schema key is missing from the root, the JSON Schema is not valid") 21 | } 22 | if validerr != nil { 23 | t.Error("It should be possible to parse a simple JSON schema if the $schema key is present") 24 | } 25 | } 26 | 27 | func TestThatTheRootSchemaCanBeParsed(t *testing.T) { 28 | s := `{ 29 | "$schema": "http://json-schema.org/schema#", 30 | "title": "root" 31 | }` 32 | so, err := Parse(s, &url.URL{Scheme: "file", Path: "jsonschemaparse_test.go"}) 33 | 34 | if err != nil { 35 | t.Fatal("It should be possible to unmarshal a simple schema, but received error:", err) 36 | } 37 | 38 | if so.Title != "root" { 39 | t.Errorf("The title was not deserialised from the JSON schema, expected %s, but got %s", "root", so.Title) 40 | } 41 | } 42 | 43 | func TestThatPropertiesCanBeParsed(t *testing.T) { 44 | s := `{ 45 | "$schema": "http://json-schema.org/schema#", 46 | "title": "root", 47 | "properties": { 48 | "name": { 49 | "type": "string" 50 | }, 51 | "address": { 52 | "$ref": "#/definitions/address" 53 | }, 54 | "status": { 55 | "$ref": "#/definitions/status" 56 | } 57 | } 58 | }` 59 | so, err := Parse(s, &url.URL{Scheme: "file", Path: "jsonschemaparse_test.go"}) 60 | 61 | if err != nil { 62 | t.Fatal("It was not possible to unmarshal the schema:", err) 63 | } 64 | 65 | nameType, nameMultiple := so.Properties["name"].Type() 66 | if nameType != "string" || nameMultiple { 67 | t.Errorf("expected property 'name' type to be 'string', but was '%v'", nameType) 68 | } 69 | 70 | addressType, _ := so.Properties["address"].Type() 71 | if addressType != "" { 72 | t.Errorf("expected property 'address' type to be '', but was '%v'", addressType) 73 | } 74 | 75 | if so.Properties["address"].Reference != "#/definitions/address" { 76 | t.Errorf("expected property 'address' reference to be '#/definitions/address', but was '%v'", so.Properties["address"].Reference) 77 | } 78 | 79 | if so.Properties["status"].Reference != "#/definitions/status" { 80 | t.Errorf("expected property 'status' reference to be '#/definitions/status', but was '%v'", so.Properties["status"].Reference) 81 | } 82 | } 83 | 84 | func TestThatPropertiesCanHaveMultipleTypes(t *testing.T) { 85 | s := `{ 86 | "$schema": "http://json-schema.org/schema#", 87 | "title": "root", 88 | "properties": { 89 | "name": { 90 | "type": [ "integer", "string" ] 91 | } 92 | } 93 | }` 94 | so, err := Parse(s, &url.URL{Scheme: "file", Path: "jsonschemaparse_test.go"}) 95 | 96 | if err != nil { 97 | t.Fatal("It was not possible to unmarshal the schema:", err) 98 | } 99 | 100 | nameType, nameMultiple := so.Properties["name"].Type() 101 | if nameType != "integer" { 102 | t.Errorf("expected first value of property 'name' type to be 'integer', but was '%v'", nameType) 103 | } 104 | 105 | if !nameMultiple { 106 | t.Errorf("expected multiple types, but only returned one") 107 | } 108 | } 109 | 110 | func TestThatParsingInvalidValuesReturnsAnError(t *testing.T) { 111 | s := `{ " }` 112 | _, err := Parse(s, &url.URL{Scheme: "file", Path: "jsonschemaparse_test.go"}) 113 | 114 | if err == nil { 115 | t.Fatal("Expected a parsing error, but got nil") 116 | } 117 | } 118 | 119 | func TestThatDefaultsCanBeParsed(t *testing.T) { 120 | s := `{ 121 | "$schema": "http://json-schema.org/schema#", 122 | "title": "root", 123 | "properties": { 124 | "name": { 125 | "type": [ "integer", "string" ], 126 | "default":"Enrique" 127 | } 128 | } 129 | }` 130 | so, err := Parse(s, &url.URL{Scheme: "file", Path: "jsonschemaparse_test.go"}) 131 | 132 | if err != nil { 133 | t.Fatal("It was not possible to unmarshal the schema:", err) 134 | } 135 | 136 | defaultValue := so.Properties["name"].Default 137 | if defaultValue != "Enrique" { 138 | t.Errorf("expected default value of property 'name' type to be 'Enrique', but was '%v'", defaultValue) 139 | } 140 | } 141 | 142 | func TestReturnedSchemaId(t *testing.T) { 143 | tests := []struct { 144 | input *Schema 145 | expected string 146 | }{ 147 | { 148 | input: &Schema{}, 149 | expected: "", 150 | }, 151 | { 152 | input: &Schema{ID06: "http://example.com/foo.json", ID04: "#foo"}, 153 | expected: "http://example.com/foo.json", 154 | }, 155 | { 156 | input: &Schema{ID04: "#foo"}, 157 | expected: "#foo", 158 | }, 159 | } 160 | 161 | for idx, test := range tests { 162 | actual := test.input.ID() 163 | if actual != test.expected { 164 | t.Errorf("Test %d failed: For input \"%+v\", expected \"%s\", got \"%s\"", idx, test.input, test.expected, actual) 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /test/additionalPropertiesMarshal_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "testing" 5 | gen "github.com/a-h/generate/test/additionalPropertiesMarshal_gen" 6 | "encoding/json" 7 | "reflect" 8 | ) 9 | 10 | func TestApRefNoProp(t *testing.T) { 11 | } 12 | 13 | func TestApRefProp(t *testing.T) { 14 | } 15 | 16 | func TestApRefReqProp(t *testing.T) { 17 | } 18 | 19 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 20 | 21 | func TestApTrueNoProp(t *testing.T) { 22 | 23 | noPropData := `{"a": "b", "c": 42 }` 24 | 25 | ap := gen.ApTrueNoProp{} 26 | err := json.Unmarshal([]byte(noPropData), &ap) 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | 31 | if len(ap.AdditionalProperties) != 2 { 32 | t.Fatalf("Wrong number of additionalProperties: %d", len(ap.AdditionalProperties)) 33 | } 34 | 35 | if s, ok := ap.AdditionalProperties["a"].(string); !ok { 36 | t.Fatalf("a was not a string") 37 | } else { 38 | if s != "b" { 39 | t.Fatalf("wrong value for a: \"%s\" (should be \"b\")", s) 40 | } 41 | } 42 | 43 | typ := reflect.TypeOf(ap.AdditionalProperties["c"]) 44 | 45 | if c, ok := ap.AdditionalProperties["c"].(float64); !ok { 46 | t.Fatalf("c was not an number (it was a %s)", typ.Name()) 47 | } else { 48 | if c != 42 { 49 | t.Fatalf("wrong value for c: \"%f\" (should be \"42\")", c) 50 | } 51 | } 52 | } 53 | 54 | 55 | func TestApTrueProp(t *testing.T) { 56 | data := `{"a": "b", "c": 42, "stuff": "xyz" }` 57 | 58 | ap := gen.ApTrueProp{} 59 | err := json.Unmarshal([]byte(data), &ap) 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | 64 | if len(ap.AdditionalProperties) != 2 { 65 | t.Fatalf("Wrong number of additionalProperties: %d", len(ap.AdditionalProperties)) 66 | } 67 | 68 | if s, ok := ap.AdditionalProperties["a"].(string); !ok { 69 | t.Fatalf("a was not a string") 70 | } else { 71 | if s != "b" { 72 | t.Fatalf("wrong value for a: \"%s\" (should be \"b\")", s) 73 | } 74 | } 75 | 76 | typ := reflect.TypeOf(ap.AdditionalProperties["c"]) 77 | 78 | if c, ok := ap.AdditionalProperties["c"].(float64); !ok { 79 | t.Fatalf("c was not an number (it was a %s)", typ.Name()) 80 | } else { 81 | if c != 42 { 82 | t.Fatalf("wrong value for c: \"%f\" (should be \"42\")", c) 83 | } 84 | } 85 | 86 | if ap.Stuff != "xyz" { 87 | t.Fatalf("invalid stuff value: \"%s\"", ap.Stuff) 88 | } 89 | } 90 | 91 | func TestApTrueReqProp(t *testing.T) { 92 | dataGood := `{"a": "b", "c": 42, "stuff": "xyz" }` 93 | dataBad := `{"a": "b", "c": 42 }` 94 | 95 | { 96 | ap := gen.ApTrueReqProp{} 97 | err := json.Unmarshal([]byte(dataBad), &ap) 98 | if err == nil { 99 | t.Fatalf("should have returned an error, required field missing") 100 | } 101 | } 102 | 103 | ap := gen.ApTrueReqProp{} 104 | err := json.Unmarshal([]byte(dataGood), &ap) 105 | if err != nil { 106 | t.Fatal(err) 107 | } 108 | 109 | if len(ap.AdditionalProperties) != 2 { 110 | t.Fatalf("Wrong number of additionalProperties: %d", len(ap.AdditionalProperties)) 111 | } 112 | 113 | if s, ok := ap.AdditionalProperties["a"].(string); !ok { 114 | t.Fatalf("a was not a string") 115 | } else { 116 | if s != "b" { 117 | t.Fatalf("wrong value for a: \"%s\" (should be \"b\")", s) 118 | } 119 | } 120 | 121 | typ := reflect.TypeOf(ap.AdditionalProperties["c"]) 122 | 123 | if c, ok := ap.AdditionalProperties["c"].(float64); !ok { 124 | t.Fatalf("c was not an number (it was a %s)", typ.Name()) 125 | } else { 126 | if c != 42 { 127 | t.Fatalf("wrong value for c: \"%f\" (should be \"42\")", c) 128 | } 129 | } 130 | 131 | if ap.Stuff != "xyz" { 132 | t.Fatalf("invalid stuff value: \"%s\"", ap.Stuff) 133 | } 134 | } 135 | 136 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 137 | 138 | func TestApFalseNoProp(t *testing.T) { 139 | 140 | dataBad1 := `{"a": "b", "c": 42, "stuff": "xyz"}` 141 | dataBad2 := `{"a": "b", "c": 42}` 142 | dataBad3 := `{"stuff": "xyz"}` 143 | dataGood1 := `{}` 144 | 145 | { 146 | ap := gen.ApFalseNoProp{} 147 | err := json.Unmarshal([]byte(dataBad1), &ap) 148 | if err == nil { 149 | t.Fatalf("should have returned an error, required field missing") 150 | } 151 | } 152 | 153 | { 154 | ap := gen.ApFalseNoProp{} 155 | err := json.Unmarshal([]byte(dataBad2), &ap) 156 | if err == nil { 157 | t.Fatalf("should have returned an error, required field missing") 158 | } 159 | } 160 | 161 | { 162 | ap := gen.ApFalseNoProp{} 163 | err := json.Unmarshal([]byte(dataBad3), &ap) 164 | if err == nil { 165 | t.Fatalf("should have returned an error, required field missing") 166 | } 167 | } 168 | 169 | { 170 | ap := gen.ApFalseNoProp{} 171 | err := json.Unmarshal([]byte(dataGood1), &ap) 172 | if err != nil { 173 | t.Fatal(err) 174 | } 175 | } 176 | } 177 | 178 | 179 | func TestApFalseProp(t *testing.T) { 180 | dataBad1 := `{"a": "b", "c": 42, "stuff": "xyz"}` 181 | dataBad2 := `{"a": "b", "c": 42}` 182 | dataGood1 := `{"stuff": "xyz"}` 183 | dataGood2 := `{}` 184 | 185 | { 186 | ap := gen.ApFalseProp{} 187 | err := json.Unmarshal([]byte(dataBad1), &ap) 188 | if err == nil { 189 | t.Fatalf("should have returned an error, required field missing") 190 | } 191 | } 192 | 193 | { 194 | ap := gen.ApFalseProp{} 195 | err := json.Unmarshal([]byte(dataBad2), &ap) 196 | if err == nil { 197 | t.Fatalf("should have returned an error, required field missing") 198 | } 199 | } 200 | 201 | { 202 | ap := gen.ApFalseProp{} 203 | err := json.Unmarshal([]byte(dataGood1), &ap) 204 | if err != nil { 205 | t.Fatal(err) 206 | } 207 | if ap.Stuff != "xyz" { 208 | t.Fatalf("invalid stuff value: \"%s\"", ap.Stuff) 209 | } 210 | } 211 | 212 | { 213 | ap := gen.ApFalseProp{} 214 | err := json.Unmarshal([]byte(dataGood2), &ap) 215 | if err != nil { 216 | t.Fatal(err) 217 | } 218 | if ap.Stuff != "" { 219 | t.Fatalf("invalid stuff value: \"%s\"", ap.Stuff) 220 | } 221 | } 222 | 223 | ap := gen.ApFalseProp{} 224 | if _, ok := reflect.TypeOf(ap).FieldByName("AdditionalProperties"); ok { 225 | t.Fatalf("AdditionalProperties was generated where it should not have been") 226 | } 227 | } 228 | 229 | func TestApFalseReqProp(t *testing.T) { 230 | dataBad1 := `{"a": "b", "c": 42, "stuff": "xyz"}` 231 | dataBad2 := `{"a": "b", "c": 42}` 232 | dataBad3 := `{}` 233 | dataGood := `{"stuff": "xyz"}` 234 | 235 | { 236 | ap := gen.ApFalseReqProp{} 237 | err := json.Unmarshal([]byte(dataBad1), &ap) 238 | if err == nil { 239 | t.Fatalf("should have returned an error, required field missing") 240 | } 241 | } 242 | 243 | { 244 | ap := gen.ApFalseReqProp{} 245 | err := json.Unmarshal([]byte(dataBad2), &ap) 246 | if err == nil { 247 | t.Fatalf("should have returned an error, required field missing") 248 | } 249 | } 250 | 251 | { 252 | ap := gen.ApFalseReqProp{} 253 | err := json.Unmarshal([]byte(dataBad3), &ap) 254 | if err == nil { 255 | t.Fatalf("should have returned an error, required field missing") 256 | } 257 | } 258 | 259 | ap := gen.ApFalseReqProp{} 260 | err := json.Unmarshal([]byte(dataGood), &ap) 261 | if err != nil { 262 | t.Fatal(err) 263 | } 264 | 265 | if _, ok := reflect.TypeOf(ap).FieldByName("AdditionalProperties"); ok { 266 | t.Fatalf("AdditionalProperties was generated where it should not have been") 267 | } 268 | 269 | if ap.Stuff != "xyz" { 270 | t.Fatalf("invalid stuff value: \"%s\"", ap.Stuff) 271 | } 272 | } -------------------------------------------------------------------------------- /output.go: -------------------------------------------------------------------------------- 1 | package generate 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "sort" 8 | "strings" 9 | ) 10 | 11 | func getOrderedFieldNames(m map[string]Field) []string { 12 | keys := make([]string, len(m)) 13 | idx := 0 14 | for k := range m { 15 | keys[idx] = k 16 | idx++ 17 | } 18 | sort.Strings(keys) 19 | return keys 20 | } 21 | 22 | func getOrderedStructNames(m map[string]Struct) []string { 23 | keys := make([]string, len(m)) 24 | idx := 0 25 | for k := range m { 26 | keys[idx] = k 27 | idx++ 28 | } 29 | sort.Strings(keys) 30 | return keys 31 | } 32 | 33 | // Output generates code and writes to w. 34 | func Output(w io.Writer, g *Generator, pkg string) { 35 | structs := g.Structs 36 | aliases := g.Aliases 37 | 38 | fmt.Fprintln(w, "// Code generated by schema-generate. DO NOT EDIT.") 39 | fmt.Fprintln(w) 40 | fmt.Fprintf(w, "package %v\n", cleanPackageName(pkg)) 41 | 42 | // write all the code into a buffer, compiler functions will return list of imports 43 | // write list of imports into main output stream, followed by the code 44 | codeBuf := new(bytes.Buffer) 45 | imports := make(map[string]bool) 46 | 47 | for _, k := range getOrderedStructNames(structs) { 48 | s := structs[k] 49 | if s.GenerateCode { 50 | emitMarshalCode(codeBuf, s, imports) 51 | emitUnmarshalCode(codeBuf, s, imports) 52 | } 53 | } 54 | 55 | if len(imports) > 0 { 56 | fmt.Fprintf(w, "\nimport (\n") 57 | for k := range imports { 58 | fmt.Fprintf(w, " \"%s\"\n", k) 59 | } 60 | fmt.Fprintf(w, ")\n") 61 | } 62 | 63 | for _, k := range getOrderedFieldNames(aliases) { 64 | a := aliases[k] 65 | 66 | fmt.Fprintln(w, "") 67 | fmt.Fprintf(w, "// %s\n", a.Name) 68 | fmt.Fprintf(w, "type %s %s\n", a.Name, a.Type) 69 | } 70 | 71 | for _, k := range getOrderedStructNames(structs) { 72 | s := structs[k] 73 | 74 | fmt.Fprintln(w, "") 75 | outputNameAndDescriptionComment(s.Name, s.Description, w) 76 | fmt.Fprintf(w, "type %s struct {\n", s.Name) 77 | 78 | for _, fieldKey := range getOrderedFieldNames(s.Fields) { 79 | f := s.Fields[fieldKey] 80 | 81 | // Only apply omitempty if the field is not required. 82 | omitempty := ",omitempty" 83 | if f.Required { 84 | omitempty = "" 85 | } 86 | 87 | if f.Description != "" { 88 | outputFieldDescriptionComment(f.Description, w) 89 | } 90 | 91 | fmt.Fprintf(w, " %s %s `json:\"%s%s\"`\n", f.Name, f.Type, f.JSONName, omitempty) 92 | } 93 | 94 | fmt.Fprintln(w, "}") 95 | } 96 | 97 | // write code after structs for clarity 98 | w.Write(codeBuf.Bytes()) 99 | } 100 | 101 | func emitMarshalCode(w io.Writer, s Struct, imports map[string]bool) { 102 | imports["bytes"] = true 103 | fmt.Fprintf(w, 104 | ` 105 | func (strct *%s) MarshalJSON() ([]byte, error) { 106 | buf := bytes.NewBuffer(make([]byte, 0)) 107 | buf.WriteString("{") 108 | `, s.Name) 109 | 110 | if len(s.Fields) > 0 { 111 | fmt.Fprintf(w, " comma := false\n") 112 | // Marshal all the defined fields 113 | for _, fieldKey := range getOrderedFieldNames(s.Fields) { 114 | f := s.Fields[fieldKey] 115 | if f.JSONName == "-" { 116 | continue 117 | } 118 | if f.Required { 119 | fmt.Fprintf(w, " // \"%s\" field is required\n", f.Name) 120 | // currently only objects are supported 121 | if strings.HasPrefix(f.Type, "*") { 122 | imports["errors"] = true 123 | fmt.Fprintf(w, ` if strct.%s == nil { 124 | return nil, errors.New("%s is a required field") 125 | } 126 | `, f.Name, f.JSONName) 127 | } else { 128 | fmt.Fprintf(w, " // only required object types supported for marshal checking (for now)\n") 129 | } 130 | } 131 | 132 | fmt.Fprintf(w, 133 | ` // Marshal the "%[1]s" field 134 | if comma { 135 | buf.WriteString(",") 136 | } 137 | buf.WriteString("\"%[1]s\": ") 138 | if tmp, err := json.Marshal(strct.%[2]s); err != nil { 139 | return nil, err 140 | } else { 141 | buf.Write(tmp) 142 | } 143 | comma = true 144 | `, f.JSONName, f.Name) 145 | } 146 | } 147 | if s.AdditionalType != "" { 148 | if s.AdditionalType != "false" { 149 | imports["fmt"] = true 150 | 151 | if len(s.Fields) == 0 { 152 | fmt.Fprintf(w, " comma := false\n") 153 | } 154 | 155 | fmt.Fprintf(w, " // Marshal any additional Properties\n") 156 | // Marshal any additional Properties 157 | fmt.Fprintf(w, ` for k, v := range strct.AdditionalProperties { 158 | if comma { 159 | buf.WriteString(",") 160 | } 161 | buf.WriteString(fmt.Sprintf("\"%%s\":", k)) 162 | if tmp, err := json.Marshal(v); err != nil { 163 | return nil, err 164 | } else { 165 | buf.Write(tmp) 166 | } 167 | comma = true 168 | } 169 | `) 170 | } 171 | } 172 | 173 | fmt.Fprintf(w, ` 174 | buf.WriteString("}") 175 | rv := buf.Bytes() 176 | return rv, nil 177 | } 178 | `) 179 | } 180 | 181 | func emitUnmarshalCode(w io.Writer, s Struct, imports map[string]bool) { 182 | imports["encoding/json"] = true 183 | // unmarshal code 184 | fmt.Fprintf(w, ` 185 | func (strct *%s) UnmarshalJSON(b []byte) error { 186 | `, s.Name) 187 | // setup required bools 188 | for _, fieldKey := range getOrderedFieldNames(s.Fields) { 189 | f := s.Fields[fieldKey] 190 | if f.Required { 191 | fmt.Fprintf(w, " %sReceived := false\n", f.JSONName) 192 | } 193 | } 194 | // setup initial unmarshal 195 | fmt.Fprintf(w, ` var jsonMap map[string]json.RawMessage 196 | if err := json.Unmarshal(b, &jsonMap); err != nil { 197 | return err 198 | }`) 199 | 200 | // figure out if we need the "v" output of the range keyword 201 | needVal := "_" 202 | if len(s.Fields) > 0 || s.AdditionalType != "false" { 203 | needVal = "v" 204 | } 205 | // start the loop 206 | fmt.Fprintf(w, ` 207 | // parse all the defined properties 208 | for k, %s := range jsonMap { 209 | switch k { 210 | `, needVal) 211 | // handle defined properties 212 | for _, fieldKey := range getOrderedFieldNames(s.Fields) { 213 | f := s.Fields[fieldKey] 214 | if f.JSONName == "-" { 215 | continue 216 | } 217 | fmt.Fprintf(w, ` case "%s": 218 | if err := json.Unmarshal([]byte(v), &strct.%s); err != nil { 219 | return err 220 | } 221 | `, f.JSONName, f.Name) 222 | if f.Required { 223 | fmt.Fprintf(w, " %sReceived = true\n", f.JSONName) 224 | } 225 | } 226 | 227 | // handle additional property 228 | if s.AdditionalType != "" { 229 | if s.AdditionalType == "false" { 230 | // all unknown properties are not allowed 231 | imports["fmt"] = true 232 | fmt.Fprintf(w, ` default: 233 | return fmt.Errorf("additional property not allowed: \"" + k + "\"") 234 | `) 235 | } else { 236 | fmt.Fprintf(w, ` default: 237 | // an additional "%s" value 238 | var additionalValue %s 239 | if err := json.Unmarshal([]byte(v), &additionalValue); err != nil { 240 | return err // invalid additionalProperty 241 | } 242 | if strct.AdditionalProperties == nil { 243 | strct.AdditionalProperties = make(map[string]%s, 0) 244 | } 245 | strct.AdditionalProperties[k]= additionalValue 246 | `, s.AdditionalType, s.AdditionalType, s.AdditionalType) 247 | } 248 | } 249 | fmt.Fprintf(w, " }\n") // switch 250 | fmt.Fprintf(w, " }\n") // for 251 | 252 | // check all Required fields were received 253 | for _, fieldKey := range getOrderedFieldNames(s.Fields) { 254 | f := s.Fields[fieldKey] 255 | if f.Required { 256 | imports["errors"] = true 257 | fmt.Fprintf(w, ` // check if %s (a required property) was received 258 | if !%sReceived { 259 | return errors.New("\"%s\" is required but was not present") 260 | } 261 | `, f.JSONName, f.JSONName, f.JSONName) 262 | } 263 | } 264 | 265 | fmt.Fprintf(w, " return nil\n") 266 | fmt.Fprintf(w, "}\n") // UnmarshalJSON 267 | } 268 | 269 | func outputNameAndDescriptionComment(name, description string, w io.Writer) { 270 | if strings.Index(description, "\n") == -1 { 271 | fmt.Fprintf(w, "// %s %s\n", name, description) 272 | return 273 | } 274 | 275 | dl := strings.Split(description, "\n") 276 | fmt.Fprintf(w, "// %s %s\n", name, strings.Join(dl, "\n// ")) 277 | } 278 | 279 | func outputFieldDescriptionComment(description string, w io.Writer) { 280 | if strings.Index(description, "\n") == -1 { 281 | fmt.Fprintf(w, "\n // %s\n", description) 282 | return 283 | } 284 | 285 | dl := strings.Split(description, "\n") 286 | fmt.Fprintf(w, "\n // %s\n", strings.Join(dl, "\n // ")) 287 | } 288 | 289 | func cleanPackageName(pkg string) string { 290 | pkg = strings.Replace(pkg, ".", "", -1) 291 | pkg = strings.Replace(pkg, "-", "", -1) 292 | return pkg 293 | } 294 | -------------------------------------------------------------------------------- /jsonschema.go: -------------------------------------------------------------------------------- 1 | package generate 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "net/url" 7 | ) 8 | 9 | // AdditionalProperties handles additional properties present in the JSON schema. 10 | type AdditionalProperties Schema 11 | 12 | // Schema represents JSON schema. 13 | type Schema struct { 14 | // SchemaType identifies the schema version. 15 | // http://json-schema.org/draft-07/json-schema-core.html#rfc.section.7 16 | SchemaType string `json:"$schema"` 17 | 18 | // ID{04,06} is the schema URI identifier. 19 | // http://json-schema.org/draft-07/json-schema-core.html#rfc.section.8.2 20 | ID04 string `json:"id"` // up to draft-04 21 | ID06 string `json:"$id"` // from draft-06 onwards 22 | 23 | // Title and Description state the intent of the schema. 24 | Title string 25 | Description string 26 | 27 | // TypeValue is the schema instance type. 28 | // http://json-schema.org/draft-07/json-schema-validation.html#rfc.section.6.1.1 29 | TypeValue interface{} `json:"type"` 30 | 31 | // Definitions are inline re-usable schemas. 32 | // http://json-schema.org/draft-07/json-schema-validation.html#rfc.section.9 33 | Definitions map[string]*Schema 34 | 35 | // Properties, Required and AdditionalProperties describe an object's child instances. 36 | // http://json-schema.org/draft-07/json-schema-validation.html#rfc.section.6.5 37 | Properties map[string]*Schema 38 | Required []string 39 | 40 | // "additionalProperties": {...} 41 | AdditionalProperties *AdditionalProperties 42 | 43 | // "additionalProperties": false 44 | AdditionalPropertiesBool *bool `json:"-"` 45 | 46 | AnyOf []*Schema 47 | AllOf []*Schema 48 | OneOf []*Schema 49 | 50 | // Default can be used to supply a default JSON value associated with a particular schema. 51 | // http://json-schema.org/draft-07/json-schema-validation.html#rfc.section.10.2 52 | Default interface{} 53 | 54 | // Examples ... 55 | // http://json-schema.org/draft-07/json-schema-validation.html#rfc.section.10.4 56 | Examples []interface{} 57 | 58 | // Reference is a URI reference to a schema. 59 | // http://json-schema.org/draft-07/json-schema-core.html#rfc.section.8 60 | Reference string `json:"$ref"` 61 | 62 | // Items represents the types that are permitted in the array. 63 | // http://json-schema.org/draft-07/json-schema-validation.html#rfc.section.6.4 64 | Items *Schema 65 | 66 | // NameCount is the number of times the instance name was encountered across the schema. 67 | NameCount int `json:"-" ` 68 | 69 | // Parent schema 70 | Parent *Schema `json:"-" ` 71 | 72 | // Key of this schema i.e. { "JSONKey": { "type": "object", .... 73 | JSONKey string `json:"-" ` 74 | 75 | // path element - for creating a path by traversing back to the root element 76 | PathElement string `json:"-"` 77 | 78 | // calculated struct name of this object, cached here 79 | GeneratedType string `json:"-"` 80 | } 81 | 82 | // UnmarshalJSON handles unmarshalling AdditionalProperties from JSON. 83 | func (ap *AdditionalProperties) UnmarshalJSON(data []byte) error { 84 | var b bool 85 | if err := json.Unmarshal(data, &b); err == nil { 86 | *ap = (AdditionalProperties)(Schema{AdditionalPropertiesBool: &b}) 87 | return nil 88 | } 89 | 90 | // support anyOf, allOf, oneOf 91 | a := map[string][]*Schema{} 92 | if err := json.Unmarshal(data, &a); err == nil { 93 | for k, v := range a { 94 | switch k { 95 | case "oneOf": 96 | ap.OneOf = append(ap.OneOf, v...) 97 | case "allOf": 98 | ap.AllOf = append(ap.AllOf, v...) 99 | case "anyOf": 100 | ap.AnyOf = append(ap.AnyOf, v...) 101 | } 102 | } 103 | return nil 104 | } 105 | 106 | s := Schema{} 107 | err := json.Unmarshal(data, &s) 108 | if err == nil { 109 | *ap = AdditionalProperties(s) 110 | } 111 | return err 112 | } 113 | 114 | // ID returns the schema URI id. 115 | func (schema *Schema) ID() string { 116 | // prefer "$id" over "id" 117 | if schema.ID06 == "" && schema.ID04 != "" { 118 | return schema.ID04 119 | } 120 | return schema.ID06 121 | } 122 | 123 | // Type returns the type which is permitted or an empty string if the type field is missing. 124 | // The 'type' field in JSON schema also allows for a single string value or an array of strings. 125 | // Examples: 126 | // "a" => "a", false 127 | // [] => "", false 128 | // ["a"] => "a", false 129 | // ["a", "b"] => "a", true 130 | func (schema *Schema) Type() (firstOrDefault string, multiple bool) { 131 | // We've got a single value, e.g. { "type": "object" } 132 | if ts, ok := schema.TypeValue.(string); ok { 133 | firstOrDefault = ts 134 | multiple = false 135 | return 136 | } 137 | 138 | // We could have multiple types in the type value, e.g. { "type": [ "object", "array" ] } 139 | if a, ok := schema.TypeValue.([]interface{}); ok { 140 | multiple = len(a) > 1 141 | for _, n := range a { 142 | if s, ok := n.(string); ok { 143 | firstOrDefault = s 144 | return 145 | } 146 | } 147 | } 148 | 149 | return "", multiple 150 | } 151 | 152 | // MultiType returns "type" as an array 153 | func (schema *Schema) MultiType() ([]string, bool) { 154 | // We've got a single value, e.g. { "type": "object" } 155 | if ts, ok := schema.TypeValue.(string); ok { 156 | return []string{ts}, false 157 | } 158 | 159 | // We could have multiple types in the type value, e.g. { "type": [ "object", "array" ] } 160 | if a, ok := schema.TypeValue.([]interface{}); ok { 161 | rv := []string{} 162 | for _, n := range a { 163 | if s, ok := n.(string); ok { 164 | rv = append(rv, s) 165 | } 166 | } 167 | return rv, len(rv) > 1 168 | } 169 | 170 | return nil, false 171 | } 172 | 173 | // GetRoot returns the root schema. 174 | func (schema *Schema) GetRoot() *Schema { 175 | if schema.Parent != nil { 176 | return schema.Parent.GetRoot() 177 | } 178 | return schema 179 | } 180 | 181 | // Parse parses a JSON schema from a string. 182 | func Parse(schema string, uri *url.URL) (*Schema, error) { 183 | return ParseWithSchemaKeyRequired(schema, uri, true) 184 | } 185 | 186 | // ParseWithSchemaKeyRequired parses a JSON schema from a string with a flag to set whether the schema key is required. 187 | func ParseWithSchemaKeyRequired(schema string, uri *url.URL, schemaKeyRequired bool) (*Schema, error) { 188 | s := &Schema{} 189 | err := json.Unmarshal([]byte(schema), s) 190 | 191 | if err != nil { 192 | return s, err 193 | } 194 | 195 | if s.ID() == "" { 196 | s.ID06 = uri.String() 197 | } 198 | 199 | if schemaKeyRequired && s.SchemaType == "" { 200 | return s, errors.New("JSON schema must have a $schema key unless schemaKeyRequired flag is set") 201 | } 202 | 203 | // validate root URI, it MUST be an absolute URI 204 | abs, err := url.Parse(s.ID()) 205 | if err != nil { 206 | return nil, errors.New("error parsing $id of document \"" + uri.String() + "\": " + err.Error()) 207 | } 208 | if !abs.IsAbs() { 209 | return nil, errors.New("$id of document not absolute URI: \"" + uri.String() + "\": \"" + s.ID() + "\"") 210 | } 211 | 212 | s.Init() 213 | 214 | return s, nil 215 | } 216 | 217 | // Init schema. 218 | func (schema *Schema) Init() { 219 | root := schema.GetRoot() 220 | root.updateParentLinks() 221 | root.ensureSchemaKeyword() 222 | root.updatePathElements() 223 | } 224 | 225 | func (schema *Schema) updatePathElements() { 226 | if schema.IsRoot() { 227 | schema.PathElement = "#" 228 | } 229 | 230 | for k, d := range schema.Definitions { 231 | d.PathElement = "definitions/" + k 232 | d.updatePathElements() 233 | } 234 | 235 | for k, p := range schema.Properties { 236 | p.PathElement = "properties/" + k 237 | p.updatePathElements() 238 | } 239 | 240 | if schema.AdditionalProperties != nil { 241 | schema.AdditionalProperties.PathElement = "additionalProperties" 242 | (*Schema)(schema.AdditionalProperties).updatePathElements() 243 | } 244 | 245 | if schema.Items != nil { 246 | schema.Items.PathElement = "items" 247 | schema.Items.updatePathElements() 248 | } 249 | } 250 | 251 | func (schema *Schema) updateParentLinks() { 252 | for k, d := range schema.Definitions { 253 | d.JSONKey = k 254 | d.Parent = schema 255 | d.updateParentLinks() 256 | } 257 | 258 | for k, p := range schema.Properties { 259 | p.JSONKey = k 260 | p.Parent = schema 261 | p.updateParentLinks() 262 | } 263 | if schema.AdditionalProperties != nil { 264 | schema.AdditionalProperties.Parent = schema 265 | (*Schema)(schema.AdditionalProperties).updateParentLinks() 266 | } 267 | if schema.Items != nil { 268 | schema.Items.Parent = schema 269 | schema.Items.updateParentLinks() 270 | } 271 | } 272 | 273 | func (schema *Schema) ensureSchemaKeyword() error { 274 | check := func(k string, s *Schema) error { 275 | if s.SchemaType != "" { 276 | return errors.New("invalid $schema keyword: " + k) 277 | } 278 | return s.ensureSchemaKeyword() 279 | } 280 | for k, d := range schema.Definitions { 281 | if err := check(k, d); err != nil { 282 | return err 283 | } 284 | } 285 | for k, d := range schema.Properties { 286 | if err := check(k, d); err != nil { 287 | return err 288 | } 289 | } 290 | if schema.AdditionalProperties != nil { 291 | if err := check("additionalProperties", (*Schema)(schema.AdditionalProperties)); err != nil { 292 | return err 293 | } 294 | } 295 | if schema.Items != nil { 296 | if err := check("items", schema.Items); err != nil { 297 | return err 298 | } 299 | } 300 | return nil 301 | } 302 | 303 | // FixMissingTypeValue is backwards compatible, guessing the users intention when they didn't specify a type. 304 | func (schema *Schema) FixMissingTypeValue() { 305 | if schema.TypeValue == nil { 306 | if schema.Reference == "" && len(schema.Properties) > 0 { 307 | schema.TypeValue = "object" 308 | return 309 | } 310 | if schema.Items != nil { 311 | schema.TypeValue = "array" 312 | return 313 | } 314 | } 315 | } 316 | 317 | // IsRoot returns true when the schema is the root. 318 | func (schema *Schema) IsRoot() bool { 319 | return schema.Parent == nil 320 | } 321 | -------------------------------------------------------------------------------- /generator.go: -------------------------------------------------------------------------------- 1 | package generate 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | "unicode" 9 | ) 10 | 11 | // Generator will produce structs from the JSON schema. 12 | type Generator struct { 13 | schemas []*Schema 14 | resolver *RefResolver 15 | Structs map[string]Struct 16 | Aliases map[string]Field 17 | // cache for reference types; k=url v=type 18 | refs map[string]string 19 | anonCount int 20 | } 21 | 22 | // New creates an instance of a generator which will produce structs. 23 | func New(schemas ...*Schema) *Generator { 24 | return &Generator{ 25 | schemas: schemas, 26 | resolver: NewRefResolver(schemas), 27 | Structs: make(map[string]Struct), 28 | Aliases: make(map[string]Field), 29 | refs: make(map[string]string), 30 | } 31 | } 32 | 33 | // CreateTypes creates types from the JSON schemas, keyed by the golang name. 34 | func (g *Generator) CreateTypes() (err error) { 35 | if err := g.resolver.Init(); err != nil { 36 | return err 37 | } 38 | 39 | // extract the types 40 | for _, schema := range g.schemas { 41 | name := g.getSchemaName("", schema) 42 | rootType, err := g.processSchema(name, schema) 43 | if err != nil { 44 | return err 45 | } 46 | // ugh: if it was anything but a struct the type will not be the name... 47 | if rootType != "*"+name { 48 | a := Field{ 49 | Name: name, 50 | JSONName: "", 51 | Type: rootType, 52 | Required: false, 53 | Description: schema.Description, 54 | } 55 | g.Aliases[a.Name] = a 56 | } 57 | } 58 | return 59 | } 60 | 61 | // process a block of definitions 62 | func (g *Generator) processDefinitions(schema *Schema) error { 63 | for key, subSchema := range schema.Definitions { 64 | if _, err := g.processSchema(getGolangName(key), subSchema); err != nil { 65 | return err 66 | } 67 | } 68 | return nil 69 | } 70 | 71 | // process a reference string 72 | func (g *Generator) processReference(schema *Schema) (string, error) { 73 | schemaPath := g.resolver.GetPath(schema) 74 | if schema.Reference == "" { 75 | return "", errors.New("processReference empty reference: " + schemaPath) 76 | } 77 | refSchema, err := g.resolver.GetSchemaByReference(schema) 78 | if err != nil { 79 | return "", errors.New("processReference: reference \"" + schema.Reference + "\" not found at \"" + schemaPath + "\"") 80 | } 81 | if refSchema.GeneratedType == "" { 82 | // reference is not resolved yet. Do that now. 83 | refSchemaName := g.getSchemaName("", refSchema) 84 | typeName, err := g.processSchema(refSchemaName, refSchema) 85 | if err != nil { 86 | return "", err 87 | } 88 | return typeName, nil 89 | } 90 | return refSchema.GeneratedType, nil 91 | } 92 | 93 | // returns the type refered to by schema after resolving all dependencies 94 | func (g *Generator) processSchema(schemaName string, schema *Schema) (typ string, err error) { 95 | if len(schema.Definitions) > 0 { 96 | g.processDefinitions(schema) 97 | } 98 | schema.FixMissingTypeValue() 99 | // if we have multiple schema types, the golang type will be interface{} 100 | typ = "interface{}" 101 | types, isMultiType := schema.MultiType() 102 | if len(types) > 0 { 103 | for _, schemaType := range types { 104 | name := schemaName 105 | if isMultiType { 106 | name = name + "_" + schemaType 107 | } 108 | switch schemaType { 109 | case "object": 110 | rv, err := g.processObject(name, schema) 111 | if err != nil { 112 | return "", err 113 | } 114 | if !isMultiType { 115 | return rv, nil 116 | } 117 | case "array": 118 | rv, err := g.processArray(name, schema) 119 | if err != nil { 120 | return "", err 121 | } 122 | if !isMultiType { 123 | return rv, nil 124 | } 125 | default: 126 | rv, err := getPrimitiveTypeName(schemaType, "", false) 127 | if err != nil { 128 | return "", err 129 | } 130 | if !isMultiType { 131 | return rv, nil 132 | } 133 | } 134 | } 135 | } else { 136 | if schema.Reference != "" { 137 | return g.processReference(schema) 138 | } 139 | } 140 | return // return interface{} 141 | } 142 | 143 | // name: name of this array, usually the js key 144 | // schema: items element 145 | func (g *Generator) processArray(name string, schema *Schema) (typeStr string, err error) { 146 | if schema.Items != nil { 147 | // subType: fallback name in case this array contains inline object without a title 148 | subName := g.getSchemaName(name+"Items", schema.Items) 149 | subTyp, err := g.processSchema(subName, schema.Items) 150 | if err != nil { 151 | return "", err 152 | } 153 | finalType, err := getPrimitiveTypeName("array", subTyp, true) 154 | if err != nil { 155 | return "", err 156 | } 157 | // only alias root arrays 158 | if schema.Parent == nil { 159 | array := Field{ 160 | Name: name, 161 | JSONName: "", 162 | Type: finalType, 163 | Required: contains(schema.Required, name), 164 | Description: schema.Description, 165 | } 166 | g.Aliases[array.Name] = array 167 | } 168 | return finalType, nil 169 | } 170 | return "[]interface{}", nil 171 | } 172 | 173 | // name: name of the struct (calculated by caller) 174 | // schema: detail incl properties & child objects 175 | // returns: generated type 176 | func (g *Generator) processObject(name string, schema *Schema) (typ string, err error) { 177 | strct := Struct{ 178 | ID: schema.ID(), 179 | Name: name, 180 | Description: schema.Description, 181 | Fields: make(map[string]Field, len(schema.Properties)), 182 | } 183 | // cache the object name in case any sub-schemas recursively reference it 184 | schema.GeneratedType = "*" + name 185 | // regular properties 186 | for propKey, prop := range schema.Properties { 187 | fieldName := getGolangName(propKey) 188 | // calculate sub-schema name here, may not actually be used depending on type of schema! 189 | subSchemaName := g.getSchemaName(fieldName, prop) 190 | fieldType, err := g.processSchema(subSchemaName, prop) 191 | if err != nil { 192 | return "", err 193 | } 194 | f := Field{ 195 | Name: fieldName, 196 | JSONName: propKey, 197 | Type: fieldType, 198 | Required: contains(schema.Required, propKey), 199 | Description: prop.Description, 200 | } 201 | if f.Required { 202 | strct.GenerateCode = true 203 | } 204 | strct.Fields[f.Name] = f 205 | } 206 | // additionalProperties with typed sub-schema 207 | if schema.AdditionalProperties != nil && schema.AdditionalProperties.AdditionalPropertiesBool == nil { 208 | ap := (*Schema)(schema.AdditionalProperties) 209 | apName := g.getSchemaName("", ap) 210 | subTyp, err := g.processSchema(apName, ap) 211 | if err != nil { 212 | return "", err 213 | } 214 | mapTyp := "map[string]" + subTyp 215 | // If this object is inline property for another object, and only contains additional properties, we can 216 | // collapse the structure down to a map. 217 | // 218 | // If this object is a definition and only contains additional properties, we can't do that or we end up with 219 | // no struct 220 | isDefinitionObject := strings.HasPrefix(schema.PathElement, "definitions") 221 | if len(schema.Properties) == 0 && !isDefinitionObject { 222 | // since there are no regular properties, we don't need to emit a struct for this object - return the 223 | // additionalProperties map type. 224 | return mapTyp, nil 225 | } 226 | // this struct will have both regular and additional properties 227 | f := Field{ 228 | Name: "AdditionalProperties", 229 | JSONName: "-", 230 | Type: mapTyp, 231 | Required: false, 232 | Description: "", 233 | } 234 | strct.Fields[f.Name] = f 235 | // setting this will cause marshal code to be emitted in Output() 236 | strct.GenerateCode = true 237 | strct.AdditionalType = subTyp 238 | } 239 | // additionalProperties as either true (everything) or false (nothing) 240 | if schema.AdditionalProperties != nil && schema.AdditionalProperties.AdditionalPropertiesBool != nil { 241 | if *schema.AdditionalProperties.AdditionalPropertiesBool == true { 242 | // everything is valid additional 243 | subTyp := "map[string]interface{}" 244 | f := Field{ 245 | Name: "AdditionalProperties", 246 | JSONName: "-", 247 | Type: subTyp, 248 | Required: false, 249 | Description: "", 250 | } 251 | strct.Fields[f.Name] = f 252 | // setting this will cause marshal code to be emitted in Output() 253 | strct.GenerateCode = true 254 | strct.AdditionalType = "interface{}" 255 | } else { 256 | // nothing 257 | strct.GenerateCode = true 258 | strct.AdditionalType = "false" 259 | } 260 | } 261 | g.Structs[strct.Name] = strct 262 | // objects are always a pointer 263 | return getPrimitiveTypeName("object", name, true) 264 | } 265 | 266 | func contains(s []string, e string) bool { 267 | for _, a := range s { 268 | if a == e { 269 | return true 270 | } 271 | } 272 | return false 273 | } 274 | 275 | func getPrimitiveTypeName(schemaType string, subType string, pointer bool) (name string, err error) { 276 | switch schemaType { 277 | case "array": 278 | if subType == "" { 279 | return "error_creating_array", errors.New("can't create an array of an empty subtype") 280 | } 281 | return "[]" + subType, nil 282 | case "boolean": 283 | return "bool", nil 284 | case "integer": 285 | return "int", nil 286 | case "number": 287 | return "float64", nil 288 | case "null": 289 | return "nil", nil 290 | case "object": 291 | if subType == "" { 292 | return "error_creating_object", errors.New("can't create an object of an empty subtype") 293 | } 294 | if pointer { 295 | return "*" + subType, nil 296 | } 297 | return subType, nil 298 | case "string": 299 | return "string", nil 300 | } 301 | 302 | return "undefined", fmt.Errorf("failed to get a primitive type for schemaType %s and subtype %s", 303 | schemaType, subType) 304 | } 305 | 306 | // return a name for this (sub-)schema. 307 | func (g *Generator) getSchemaName(keyName string, schema *Schema) string { 308 | if len(schema.Title) > 0 { 309 | return getGolangName(schema.Title) 310 | } 311 | if keyName != "" { 312 | return getGolangName(keyName) 313 | } 314 | if schema.Parent == nil { 315 | return "Root" 316 | } 317 | if schema.JSONKey != "" { 318 | return getGolangName(schema.JSONKey) 319 | } 320 | if schema.Parent != nil && schema.Parent.JSONKey != "" { 321 | return getGolangName(schema.Parent.JSONKey + "Item") 322 | } 323 | g.anonCount++ 324 | return fmt.Sprintf("Anonymous%d", g.anonCount) 325 | } 326 | 327 | // getGolangName strips invalid characters out of golang struct or field names. 328 | func getGolangName(s string) string { 329 | buf := bytes.NewBuffer([]byte{}) 330 | for i, v := range splitOnAll(s, isNotAGoNameCharacter) { 331 | if i == 0 && strings.IndexAny(v, "0123456789") == 0 { 332 | // Go types are not allowed to start with a number, lets prefix with an underscore. 333 | buf.WriteRune('_') 334 | } 335 | buf.WriteString(capitaliseFirstLetter(v)) 336 | } 337 | return buf.String() 338 | } 339 | 340 | func splitOnAll(s string, shouldSplit func(r rune) bool) []string { 341 | rv := []string{} 342 | buf := bytes.NewBuffer([]byte{}) 343 | for _, c := range s { 344 | if shouldSplit(c) { 345 | rv = append(rv, buf.String()) 346 | buf.Reset() 347 | } else { 348 | buf.WriteRune(c) 349 | } 350 | } 351 | if buf.Len() > 0 { 352 | rv = append(rv, buf.String()) 353 | } 354 | return rv 355 | } 356 | 357 | func isNotAGoNameCharacter(r rune) bool { 358 | if unicode.IsLetter(r) || unicode.IsDigit(r) { 359 | return false 360 | } 361 | return true 362 | } 363 | 364 | func capitaliseFirstLetter(s string) string { 365 | if s == "" { 366 | return s 367 | } 368 | prefix := s[0:1] 369 | suffix := s[1:] 370 | return strings.ToUpper(prefix) + suffix 371 | } 372 | 373 | // Struct defines the data required to generate a struct in Go. 374 | type Struct struct { 375 | // The ID within the JSON schema, e.g. #/definitions/address 376 | ID string 377 | // The golang name, e.g. "Address" 378 | Name string 379 | // Description of the struct 380 | Description string 381 | Fields map[string]Field 382 | 383 | GenerateCode bool 384 | AdditionalType string 385 | } 386 | 387 | // Field defines the data required to generate a field in Go. 388 | type Field struct { 389 | // The golang name, e.g. "Address1" 390 | Name string 391 | // The JSON name, e.g. "address1" 392 | JSONName string 393 | // The golang type of the field, e.g. a built-in type like "string" or the name of a struct generated 394 | // from the JSON schema. 395 | Type string 396 | // Required is set to true when the field is required. 397 | Required bool 398 | Description string 399 | } 400 | -------------------------------------------------------------------------------- /generator_test.go: -------------------------------------------------------------------------------- 1 | package generate 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestThatCapitalisationOccursCorrectly(t *testing.T) { 11 | tests := []struct { 12 | input string 13 | expected string 14 | }{ 15 | { 16 | input: "ssd", 17 | expected: "Ssd", 18 | }, 19 | { 20 | input: "f", 21 | expected: "F", 22 | }, 23 | { 24 | input: "fishPaste", 25 | expected: "FishPaste", 26 | }, 27 | { 28 | input: "", 29 | expected: "", 30 | }, 31 | { 32 | input: "F", 33 | expected: "F", 34 | }, 35 | } 36 | 37 | for idx, test := range tests { 38 | actual := capitaliseFirstLetter(test.input) 39 | if actual != test.expected { 40 | t.Errorf("Test %d failed: For input \"%s\", expected \"%s\", got \"%s\"", idx, test.input, test.expected, actual) 41 | } 42 | } 43 | } 44 | 45 | func TestFieldGeneration(t *testing.T) { 46 | properties := map[string]*Schema{ 47 | "property1": {TypeValue: "string"}, 48 | "property2": {Reference: "#/definitions/address"}, 49 | // test sub-objects with properties or additionalProperties 50 | "property3": {TypeValue: "object", Title: "SubObj1", Properties: map[string]*Schema{"name": {TypeValue: "string"}}}, 51 | "property4": {TypeValue: "object", Title: "SubObj2", AdditionalProperties: &AdditionalProperties{TypeValue: "integer"}}, 52 | // test sub-objects with properties composed of objects 53 | "property5": {TypeValue: "object", Title: "SubObj3", Properties: map[string]*Schema{"SubObj3a": {TypeValue: "object", Properties: map[string]*Schema{"subproperty1": {TypeValue: "integer"}}}}}, 54 | // test sub-objects with additionalProperties composed of objects 55 | "property6": {TypeValue: "object", Title: "SubObj4", AdditionalProperties: &AdditionalProperties{TypeValue: "object", Title: "SubObj4a", Properties: map[string]*Schema{"subproperty1": {TypeValue: "integer"}}}}, 56 | // test sub-objects without titles 57 | "property7": {TypeValue: "object"}, 58 | // test sub-objects with properties AND additionalProperties 59 | "property8": {TypeValue: "object", Title: "SubObj5", Properties: map[string]*Schema{"name": {TypeValue: "string"}}, AdditionalProperties: &AdditionalProperties{TypeValue: "integer"}}, 60 | } 61 | 62 | requiredFields := []string{"property2"} 63 | 64 | root := Schema{ 65 | SchemaType: "http://localhost", 66 | Title: "TestFieldGeneration", 67 | TypeValue: "object", 68 | Properties: properties, 69 | Definitions: map[string]*Schema{ 70 | "address": {TypeValue: "object"}, 71 | }, 72 | Required: requiredFields, 73 | } 74 | root.Init() 75 | g := New(&root) 76 | err := g.CreateTypes() 77 | 78 | // Output(os.Stderr, g, "test") 79 | 80 | if err != nil { 81 | t.Error("Failed to get the fields: ", err) 82 | } 83 | 84 | if len(g.Structs) != 8 { 85 | t.Errorf("Expected 8 results, but got %d results", len(g.Structs)) 86 | } 87 | 88 | testField(g.Structs["TestFieldGeneration"].Fields["Property1"], "property1", "Property1", "string", false, t) 89 | testField(g.Structs["TestFieldGeneration"].Fields["Property2"], "property2", "Property2", "*Address", true, t) 90 | testField(g.Structs["TestFieldGeneration"].Fields["Property3"], "property3", "Property3", "*SubObj1", false, t) 91 | testField(g.Structs["TestFieldGeneration"].Fields["Property4"], "property4", "Property4", "map[string]int", false, t) 92 | testField(g.Structs["TestFieldGeneration"].Fields["Property5"], "property5", "Property5", "*SubObj3", false, t) 93 | testField(g.Structs["TestFieldGeneration"].Fields["Property6"], "property6", "Property6", "map[string]*SubObj4a", false, t) 94 | testField(g.Structs["TestFieldGeneration"].Fields["Property7"], "property7", "Property7", "*Property7", false, t) 95 | testField(g.Structs["TestFieldGeneration"].Fields["Property8"], "property8", "Property8", "*SubObj5", false, t) 96 | 97 | testField(g.Structs["SubObj1"].Fields["Name"], "name", "Name", "string", false, t) 98 | testField(g.Structs["SubObj3"].Fields["SubObj3a"], "SubObj3a", "SubObj3a", "*SubObj3a", false, t) 99 | testField(g.Structs["SubObj4a"].Fields["Subproperty1"], "subproperty1", "Subproperty1", "int", false, t) 100 | 101 | testField(g.Structs["SubObj5"].Fields["Name"], "name", "Name", "string", false, t) 102 | testField(g.Structs["SubObj5"].Fields["AdditionalProperties"], "-", "AdditionalProperties", "map[string]int", false, t) 103 | 104 | if strct, ok := g.Structs["Property7"]; !ok { 105 | t.Fatal("Property7 wasn't generated") 106 | } else { 107 | if len(strct.Fields) != 0 { 108 | t.Fatal("Property7 expected 0 fields") 109 | } 110 | } 111 | } 112 | 113 | func TestFieldGenerationWithArrayReferences(t *testing.T) { 114 | properties := map[string]*Schema{ 115 | "property1": {TypeValue: "string"}, 116 | "property2": { 117 | TypeValue: "array", 118 | Items: &Schema{ 119 | Reference: "#/definitions/address", 120 | }, 121 | }, 122 | "property3": { 123 | TypeValue: "array", 124 | Items: &Schema{ 125 | TypeValue: "object", 126 | AdditionalProperties: (*AdditionalProperties)(&Schema{TypeValue: "integer"}), 127 | }, 128 | }, 129 | "property4": { 130 | TypeValue: "array", 131 | Items: &Schema{ 132 | Reference: "#/definitions/outer", 133 | }, 134 | }, 135 | } 136 | 137 | requiredFields := []string{"property2"} 138 | 139 | root := Schema{ 140 | SchemaType: "http://localhost", 141 | Title: "TestFieldGenerationWithArrayReferences", 142 | TypeValue: "object", 143 | Properties: properties, 144 | Definitions: map[string]*Schema{ 145 | "address": {TypeValue: "object"}, 146 | "outer": {TypeValue: "array", Items: &Schema{Reference: "#/definitions/inner"}}, 147 | "inner": {TypeValue: "object"}, 148 | }, 149 | Required: requiredFields, 150 | } 151 | root.Init() 152 | 153 | g := New(&root) 154 | err := g.CreateTypes() 155 | 156 | //Output(os.Stderr, g, "test") 157 | 158 | if err != nil { 159 | t.Error("Failed to get the fields: ", err) 160 | } 161 | 162 | if len(g.Structs) != 3 { 163 | t.Errorf("Expected 3 results, but got %d results", len(g.Structs)) 164 | } 165 | 166 | testField(g.Structs["TestFieldGenerationWithArrayReferences"].Fields["Property1"], "property1", "Property1", "string", false, t) 167 | testField(g.Structs["TestFieldGenerationWithArrayReferences"].Fields["Property2"], "property2", "Property2", "[]*Address", true, t) 168 | testField(g.Structs["TestFieldGenerationWithArrayReferences"].Fields["Property3"], "property3", "Property3", "[]map[string]int", false, t) 169 | testField(g.Structs["TestFieldGenerationWithArrayReferences"].Fields["Property4"], "property4", "Property4", "[][]*Inner", false, t) 170 | } 171 | 172 | func testField(actual Field, expectedJSONName string, expectedName string, expectedType string, expectedToBeRequired bool, t *testing.T) { 173 | if actual.JSONName != expectedJSONName { 174 | t.Errorf("JSONName - expected \"%s\", got \"%s\"", expectedJSONName, actual.JSONName) 175 | } 176 | if actual.Name != expectedName { 177 | t.Errorf("Name - expected \"%s\", got \"%s\"", expectedName, actual.Name) 178 | } 179 | if actual.Type != expectedType { 180 | t.Errorf("Type - expected \"%s\", got \"%s\"", expectedType, actual.Type) 181 | } 182 | if actual.Required != expectedToBeRequired { 183 | t.Errorf("Required - expected \"%v\", got \"%v\"", expectedToBeRequired, actual.Required) 184 | } 185 | } 186 | 187 | func TestNestedStructGeneration(t *testing.T) { 188 | root := &Schema{} 189 | root.Title = "Example" 190 | root.Properties = map[string]*Schema{ 191 | "property1": { 192 | TypeValue: "object", 193 | Properties: map[string]*Schema{ 194 | "subproperty1": {TypeValue: "string"}, 195 | }, 196 | }, 197 | } 198 | 199 | root.Init() 200 | 201 | g := New(root) 202 | err := g.CreateTypes() 203 | results := g.Structs 204 | 205 | //Output(os.Stderr, g, "test") 206 | 207 | if err != nil { 208 | t.Error("Failed to create structs: ", err) 209 | } 210 | 211 | if len(results) != 2 { 212 | t.Errorf("2 results should have been created, a root type and a type for the object 'property1' but %d structs were made", len(results)) 213 | } 214 | 215 | if _, contains := results["Example"]; !contains { 216 | t.Errorf("The Example type should have been made, but only types %s were made.", strings.Join(getStructNamesFromMap(results), ", ")) 217 | } 218 | 219 | if _, contains := results["Property1"]; !contains { 220 | t.Errorf("The Property1 type should have been made, but only types %s were made.", strings.Join(getStructNamesFromMap(results), ", ")) 221 | } 222 | 223 | if results["Example"].Fields["Property1"].Type != "*Property1" { 224 | t.Errorf("Expected that the nested type property1 is generated as a struct, so the property type should be *Property1, but was %s.", results["Example"].Fields["Property1"].Type) 225 | } 226 | } 227 | 228 | func TestEmptyNestedStructGeneration(t *testing.T) { 229 | root := &Schema{} 230 | root.Title = "Example" 231 | root.Properties = map[string]*Schema{ 232 | "property1": { 233 | TypeValue: "object", 234 | Properties: map[string]*Schema{ 235 | "nestedproperty1": {TypeValue: "string"}, 236 | }, 237 | }, 238 | } 239 | 240 | root.Init() 241 | 242 | g := New(root) 243 | err := g.CreateTypes() 244 | results := g.Structs 245 | 246 | // Output(os.Stderr, g, "test") 247 | 248 | if err != nil { 249 | t.Error("Failed to create structs: ", err) 250 | } 251 | 252 | if len(results) != 2 { 253 | t.Errorf("2 results should have been created, a root type and a type for the object 'property1' but %d structs were made", len(results)) 254 | } 255 | 256 | if _, contains := results["Example"]; !contains { 257 | t.Errorf("The Example type should have been made, but only types %s were made.", strings.Join(getStructNamesFromMap(results), ", ")) 258 | } 259 | 260 | if _, contains := results["Property1"]; !contains { 261 | t.Errorf("The Property1 type should have been made, but only types %s were made.", strings.Join(getStructNamesFromMap(results), ", ")) 262 | } 263 | 264 | if results["Example"].Fields["Property1"].Type != "*Property1" { 265 | t.Errorf("Expected that the nested type property1 is generated as a struct, so the property type should be *Property1, but was %s.", results["Example"].Fields["Property1"].Type) 266 | } 267 | } 268 | 269 | func TestStructNameExtractor(t *testing.T) { 270 | m := make(map[string]Struct) 271 | m["name1"] = Struct{} 272 | m["name2"] = Struct{} 273 | 274 | names := getStructNamesFromMap(m) 275 | if len(names) != 2 { 276 | t.Error("Didn't extract all names from the map.") 277 | } 278 | 279 | if !contains(names, "name1") { 280 | t.Error("name1 was not extracted") 281 | } 282 | 283 | if !contains(names, "name2") { 284 | t.Error("name2 was not extracted") 285 | } 286 | } 287 | 288 | func getStructNamesFromMap(m map[string]Struct) []string { 289 | sn := make([]string, len(m)) 290 | i := 0 291 | for k := range m { 292 | sn[i] = k 293 | i++ 294 | } 295 | return sn 296 | } 297 | 298 | func TestStructGeneration(t *testing.T) { 299 | root := &Schema{} 300 | root.Title = "RootElement" 301 | root.Definitions = make(map[string]*Schema) 302 | root.Definitions["address"] = &Schema{ 303 | Properties: map[string]*Schema{ 304 | "address1": {TypeValue: "string"}, 305 | "zip": {TypeValue: "number"}, 306 | }, 307 | } 308 | root.Properties = map[string]*Schema{ 309 | "property1": {TypeValue: "string"}, 310 | "property2": {Reference: "#/definitions/address"}, 311 | } 312 | 313 | root.Init() 314 | 315 | g := New(root) 316 | err := g.CreateTypes() 317 | results := g.Structs 318 | 319 | // Output(os.Stderr, g, "test") 320 | 321 | if err != nil { 322 | t.Error("Failed to create structs: ", err) 323 | } 324 | 325 | if len(results) != 2 { 326 | t.Error("2 results should have been created, a root type and an address") 327 | } 328 | } 329 | 330 | func TestArrayGeneration(t *testing.T) { 331 | root := &Schema{ 332 | Title: "Array of Artists Example", 333 | TypeValue: "array", 334 | Items: &Schema{ 335 | Title: "Artist", 336 | TypeValue: "object", 337 | Properties: map[string]*Schema{ 338 | "name": {TypeValue: "string"}, 339 | "birthyear": {TypeValue: "number"}, 340 | }, 341 | }, 342 | } 343 | 344 | root.Init() 345 | 346 | g := New(root) 347 | err := g.CreateTypes() 348 | results := g.Structs 349 | 350 | // Output(os.Stderr, g, "test") 351 | 352 | if err != nil { 353 | t.Fatal("Failed to create structs: ", err) 354 | } 355 | 356 | if len(results) != 1 { 357 | t.Errorf("Expected one struct should have been generated, but %d have been generated.", len(results)) 358 | } 359 | 360 | artistStruct, ok := results["Artist"] 361 | if !ok { 362 | t.Errorf("Expected Name to be Artist, that wasn't found, but the struct contains \"%+v\"", results) 363 | } 364 | 365 | if len(artistStruct.Fields) != 2 { 366 | t.Errorf("Expected the fields to be birtyear and name, but %d fields were found.", len(artistStruct.Fields)) 367 | } 368 | 369 | if _, ok := artistStruct.Fields["Name"]; !ok { 370 | t.Errorf("Expected to find a Name field, but one was not found.") 371 | } 372 | 373 | if _, ok := artistStruct.Fields["Birthyear"]; !ok { 374 | t.Errorf("Expected to find a Birthyear field, but one was not found.") 375 | } 376 | } 377 | 378 | func TestNestedArrayGeneration(t *testing.T) { 379 | root := &Schema{ 380 | Title: "Favourite Bars", 381 | TypeValue: "object", 382 | Properties: map[string]*Schema{ 383 | "barName": {TypeValue: "string"}, 384 | "cities": { 385 | TypeValue: "array", 386 | Items: &Schema{ 387 | Title: "City", 388 | TypeValue: "object", 389 | Properties: map[string]*Schema{ 390 | "name": {TypeValue: "string"}, 391 | "country": {TypeValue: "string"}, 392 | }, 393 | }, 394 | }, 395 | "tags": { 396 | TypeValue: "array", 397 | Items: &Schema{TypeValue: "string"}, 398 | }, 399 | }, 400 | } 401 | 402 | root.Init() 403 | 404 | g := New(root) 405 | err := g.CreateTypes() 406 | results := g.Structs 407 | 408 | // Output(os.Stderr, g, "test") 409 | 410 | if err != nil { 411 | t.Error("Failed to create structs: ", err) 412 | } 413 | 414 | if len(results) != 2 { 415 | t.Errorf("Expected two structs to be generated - 'Favourite Bars' and 'City', but %d have been generated.", len(results)) 416 | } 417 | 418 | fbStruct, ok := results["FavouriteBars"] 419 | if !ok { 420 | t.Errorf("FavouriteBars struct was not found. The results were %+v", results) 421 | } 422 | 423 | if _, ok := fbStruct.Fields["BarName"]; !ok { 424 | t.Errorf("Expected to find the BarName field, but didn't. The struct is %+v", fbStruct) 425 | } 426 | 427 | f, ok := fbStruct.Fields["Cities"] 428 | if !ok { 429 | t.Errorf("Expected to find the Cities field on the FavouriteBars, but didn't. The struct is %+v", fbStruct) 430 | } 431 | if f.Type != "[]*City" { 432 | t.Errorf("Expected to find that the Cities array was of type *City, but it was of %s", f.Type) 433 | } 434 | 435 | f, ok = fbStruct.Fields["Tags"] 436 | if !ok { 437 | t.Errorf("Expected to find the Tags field on the FavouriteBars, but didn't. The struct is %+v", fbStruct) 438 | } 439 | 440 | if f.Type != "[]string" { 441 | t.Errorf("Expected to find that the Tags array was of type string, but it was of %s", f.Type) 442 | } 443 | 444 | cityStruct, ok := results["City"] 445 | if !ok { 446 | t.Error("City struct was not found.") 447 | } 448 | 449 | if _, ok := cityStruct.Fields["Name"]; !ok { 450 | t.Errorf("Expected to find the Name field on the City struct, but didn't. The struct is %+v", cityStruct) 451 | } 452 | 453 | if _, ok := cityStruct.Fields["Country"]; !ok { 454 | t.Errorf("Expected to find the Country field on the City struct, but didn't. The struct is %+v", cityStruct) 455 | } 456 | } 457 | 458 | func TestMultipleSchemaStructGeneration(t *testing.T) { 459 | root1 := &Schema{ 460 | Title: "Root1Element", 461 | ID06: "http://example.com/schema/root1", 462 | Properties: map[string]*Schema{ 463 | "property1": {Reference: "root2#/definitions/address"}, 464 | }, 465 | } 466 | 467 | root2 := &Schema{ 468 | Title: "Root2Element", 469 | ID06: "http://example.com/schema/root2", 470 | Properties: map[string]*Schema{ 471 | "property1": {Reference: "#/definitions/address"}, 472 | }, 473 | Definitions: map[string]*Schema{ 474 | "address": { 475 | Properties: map[string]*Schema{ 476 | "address1": {TypeValue: "string"}, 477 | "zip": {TypeValue: "number"}, 478 | }, 479 | }, 480 | }, 481 | } 482 | 483 | root1.Init() 484 | root2.Init() 485 | 486 | g := New(root1, root2) 487 | err := g.CreateTypes() 488 | results := g.Structs 489 | 490 | // Output(os.Stderr, g, "test") 491 | 492 | if err != nil { 493 | t.Error("Failed to create structs: ", err) 494 | } 495 | 496 | if len(results) != 3 { 497 | t.Errorf("3 results should have been created, 2 root types and an address, but got %v", getStructNamesFromMap(results)) 498 | } 499 | } 500 | 501 | func TestThatJavascriptKeyNamesCanBeConvertedToValidGoNames(t *testing.T) { 502 | tests := []struct { 503 | description string 504 | input string 505 | expected string 506 | }{ 507 | { 508 | description: "Camel case is converted to pascal case.", 509 | input: "camelCase", 510 | expected: "CamelCase", 511 | }, 512 | { 513 | description: "Spaces are stripped.", 514 | input: "Contains space", 515 | expected: "ContainsSpace", 516 | }, 517 | { 518 | description: "Hyphens are stripped.", 519 | input: "key-name", 520 | expected: "KeyName", 521 | }, 522 | { 523 | description: "Underscores are stripped.", 524 | input: "key_name", 525 | expected: "KeyName", 526 | }, 527 | { 528 | description: "Periods are stripped.", 529 | input: "a.b.c", 530 | expected: "ABC", 531 | }, 532 | { 533 | description: "Colons are stripped.", 534 | input: "a:b", 535 | expected: "AB", 536 | }, 537 | { 538 | description: "GT and LT are stripped.", 539 | input: "a", 540 | expected: "AB", 541 | }, 542 | { 543 | description: "Not allowed to start with a number.", 544 | input: "123ABC", 545 | expected: "_123ABC", 546 | }, 547 | } 548 | 549 | for _, test := range tests { 550 | actual := getGolangName(test.input) 551 | 552 | if test.expected != actual { 553 | t.Errorf("For test '%s', for input '%s' expected '%s' but got '%s'.", test.description, test.input, test.expected, actual) 554 | } 555 | } 556 | } 557 | 558 | func TestThatArraysWithoutDefinedItemTypesAreGeneratedAsEmptyInterfaces(t *testing.T) { 559 | root := &Schema{} 560 | root.Title = "Array without defined item" 561 | root.Properties = map[string]*Schema{ 562 | "name": {TypeValue: "string"}, 563 | "repositories": { 564 | TypeValue: "array", 565 | }, 566 | } 567 | 568 | root.Init() 569 | 570 | g := New(root) 571 | err := g.CreateTypes() 572 | results := g.Structs 573 | 574 | // Output(os.Stderr, g, "test") 575 | 576 | if err != nil { 577 | t.Errorf("Error generating structs: %v", err) 578 | } 579 | 580 | if _, contains := results["ArrayWithoutDefinedItem"]; !contains { 581 | t.Errorf("The ArrayWithoutDefinedItem type should have been made, but only types %s were made.", strings.Join(getStructNamesFromMap(results), ", ")) 582 | } 583 | 584 | if o, ok := results["ArrayWithoutDefinedItem"]; ok { 585 | if f, ok := o.Fields["Repositories"]; ok { 586 | if f.Type != "[]interface{}" { 587 | t.Errorf("Since the schema doesn't include a type for the array items, the property type should be []interface{}, but was %s.", f.Type) 588 | } 589 | } else { 590 | t.Errorf("Expected the ArrayWithoutDefinedItem type to have a Repostitories field, but none was found.") 591 | } 592 | } 593 | } 594 | 595 | func TestThatTypesWithMultipleDefinitionsAreGeneratedAsEmptyInterfaces(t *testing.T) { 596 | root := &Schema{} 597 | root.Title = "Multiple possible types" 598 | root.Properties = map[string]*Schema{ 599 | "name": {TypeValue: []interface{}{"string", "integer"}}, 600 | } 601 | 602 | root.Init() 603 | 604 | g := New(root) 605 | err := g.CreateTypes() 606 | results := g.Structs 607 | 608 | // Output(os.Stderr, g, "test") 609 | 610 | if err != nil { 611 | t.Errorf("Error generating structs: %v", err) 612 | } 613 | 614 | if _, contains := results["MultiplePossibleTypes"]; !contains { 615 | t.Errorf("The MultiplePossibleTypes type should have been made, but only types %s were made.", strings.Join(getStructNamesFromMap(results), ", ")) 616 | } 617 | 618 | if o, ok := results["MultiplePossibleTypes"]; ok { 619 | if f, ok := o.Fields["Name"]; ok { 620 | if f.Type != "interface{}" { 621 | t.Errorf("Since the schema has multiple types for the item, the property type should be []interface{}, but was %s.", f.Type) 622 | } 623 | } else { 624 | t.Errorf("Expected the MultiplePossibleTypes type to have a Name field, but none was found.") 625 | } 626 | } 627 | } 628 | 629 | func TestThatUnmarshallingIsPossible(t *testing.T) { 630 | // { 631 | // "$schema": "http://json-schema.org/draft-04/schema#", 632 | // "name": "Example", 633 | // "type": "object", 634 | // "properties": { 635 | // "name": { 636 | // "type": ["object", "array", "integer"], 637 | // "description": "name" 638 | // } 639 | // } 640 | // } 641 | 642 | tests := []struct { 643 | name string 644 | input string 645 | expected Root 646 | }{ 647 | { 648 | name: "map", 649 | input: `{ "name": { "key": "value" } }`, 650 | expected: Root{ 651 | Name: map[string]interface{}{ 652 | "key": "value", 653 | }, 654 | }, 655 | }, 656 | { 657 | name: "array", 658 | input: `{ "name": [ "a", "b" ] }`, 659 | expected: Root{ 660 | Name: []interface{}{"a", "b"}, 661 | }, 662 | }, 663 | { 664 | name: "integer", 665 | input: `{ "name": 1 }`, 666 | expected: Root{ 667 | Name: 1.0, // can't determine whether it's a float or integer without additional info 668 | }, 669 | }, 670 | } 671 | 672 | for _, test := range tests { 673 | var actual Root 674 | err := json.Unmarshal([]byte(test.input), &actual) 675 | if err != nil { 676 | t.Errorf("%s: error unmarshalling: %v", test.name, err) 677 | } 678 | 679 | expectedType := reflect.TypeOf(test.expected.Name) 680 | actualType := reflect.TypeOf(actual.Name) 681 | if expectedType != actualType { 682 | t.Errorf("expected Name to be of type %v, but got %v", expectedType, actualType) 683 | } 684 | } 685 | } 686 | 687 | func TestTypeAliases(t *testing.T) { 688 | tests := []struct { 689 | gotype string 690 | input *Schema 691 | structs, aliases int 692 | }{ 693 | { 694 | gotype: "string", 695 | input: &Schema{TypeValue: "string"}, 696 | structs: 0, 697 | aliases: 1, 698 | }, 699 | { 700 | gotype: "int", 701 | input: &Schema{TypeValue: "integer"}, 702 | structs: 0, 703 | aliases: 1, 704 | }, 705 | { 706 | gotype: "bool", 707 | input: &Schema{TypeValue: "boolean"}, 708 | structs: 0, 709 | aliases: 1, 710 | }, 711 | { 712 | gotype: "[]*Foo", 713 | input: &Schema{TypeValue: "array", 714 | Items: &Schema{ 715 | TypeValue: "object", 716 | Title: "foo", 717 | Properties: map[string]*Schema{ 718 | "nestedproperty": {TypeValue: "string"}, 719 | }, 720 | }}, 721 | structs: 1, 722 | aliases: 1, 723 | }, 724 | { 725 | gotype: "[]interface{}", 726 | input: &Schema{TypeValue: "array"}, 727 | structs: 0, 728 | aliases: 1, 729 | }, 730 | { 731 | gotype: "map[string]string", 732 | input: &Schema{ 733 | TypeValue: "object", 734 | AdditionalProperties: (*AdditionalProperties)(&Schema{TypeValue: "string"}), 735 | }, 736 | structs: 0, 737 | aliases: 1, 738 | }, 739 | { 740 | gotype: "map[string]interface{}", 741 | input: &Schema{ 742 | TypeValue: "object", 743 | AdditionalProperties: (*AdditionalProperties)(&Schema{TypeValue: []interface{}{"string", "integer"}}), 744 | }, 745 | structs: 0, 746 | aliases: 1, 747 | }, 748 | } 749 | 750 | for _, test := range tests { 751 | test.input.Init() 752 | 753 | g := New(test.input) 754 | err := g.CreateTypes() 755 | structs := g.Structs 756 | aliases := g.Aliases 757 | 758 | // Output(os.Stderr, g, "test") 759 | 760 | if err != nil { 761 | t.Fatal(err) 762 | } 763 | 764 | if len(structs) != test.structs { 765 | t.Errorf("Expected %d structs, got %d", test.structs, len(structs)) 766 | } 767 | 768 | if len(aliases) != test.aliases { 769 | t.Errorf("Expected %d type aliases, got %d", test.aliases, len(aliases)) 770 | } 771 | 772 | if test.gotype != aliases["Root"].Type { 773 | t.Errorf("Expected Root type %q, got %q", test.gotype, aliases["Root"].Type) 774 | } 775 | } 776 | } 777 | 778 | // Root is an example of a generated type. 779 | type Root struct { 780 | Name interface{} `json:"name,omitempty"` 781 | } 782 | --------------------------------------------------------------------------------