New Upstream Release - golang-github-alecthomas-jsonschema
Ready changes
Summary
Merged new upstream version: 0.0~git20220216.9eeeec9 (was: 0.0~git20210127.19bc6f2).
Resulting package
Built on 2023-01-19T07:03 (took 3m26s)
The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:
apt install -t fresh-releases golang-github-alecthomas-jsonschema-dev
Lintian Result
- golang-github-alecthomas-jsonschema-dev_0.0~git20220216.9eeeec9+ds-1~jan+nur2_all.deb
- golang-github-alecthomas-jsonschema_0.0~git20220216.9eeeec9+ds-1~jan+nur2.dsc
- golang-github-alecthomas-jsonschema_0.0~git20220216.9eeeec9+ds-1~jan+nur2_amd64.buildinfo
- golang-github-alecthomas-jsonschema_0.0~git20220216.9eeeec9+ds-1~jan+nur2_amd64.changes
Diff
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..487674d
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1 @@
+github: [alecthomas]
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..1ce2470
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,15 @@
+on: [push, pull_request]
+name: CI
+jobs:
+ test:
+ name: Test
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v2
+ - name: Init Hermit
+ run: ./bin/hermit env --raw >> $GITHUB_ENV
+ - name: Test
+ run: go test ./...
+ - name: Lint
+ run: golangci-lint run
diff --git a/.golangci.yml b/.golangci.yml
new file mode 100644
index 0000000..df2c4fe
--- /dev/null
+++ b/.golangci.yml
@@ -0,0 +1,88 @@
+run:
+ tests: true
+ max-same-issues: 50
+ skip-dirs:
+ - resources
+ - old
+ skip-files:
+ - cmd/protopkg/main.go
+
+output:
+ print-issued-lines: false
+
+linters:
+ enable-all: true
+ disable:
+ - maligned
+ - megacheck
+ - lll
+ - typecheck # `go build` catches this, and it doesn't currently work with Go 1.11 modules
+ - goimports # horrendously slow with go modules :(
+ - dupl # has never been actually useful
+ - gochecknoglobals
+ - gochecknoinits
+ - interfacer # author deprecated it because it provides bad suggestions
+ - funlen
+ - whitespace
+ - godox
+ - wsl
+ - dogsled
+ - gomnd
+ - gocognit
+ - gocyclo
+ - scopelint
+ - godot
+ - nestif
+ - testpackage
+ - goerr113
+ - gci
+ - gofumpt
+ - exhaustivestruct
+ - nlreturn
+ - forbidigo
+ - cyclop
+ - paralleltest
+ - ifshort # so annoying
+ - golint
+ - tagliatelle
+ - forcetypeassert
+ - wrapcheck
+ - revive
+ - structcheck
+ - stylecheck
+ - exhaustive
+
+linters-settings:
+ govet:
+ check-shadowing: true
+ use-installed-packages: true
+ dupl:
+ threshold: 100
+ goconst:
+ min-len: 8
+ min-occurrences: 3
+ gocyclo:
+ min-complexity: 20
+ gocritic:
+ disabled-checks:
+ - ifElseChain
+
+
+issues:
+ max-per-linter: 0
+ max-same: 0
+ exclude-use-default: false
+ exclude:
+ # Captured by errcheck.
+ - '^(G104|G204):'
+ # Very commonly not checked.
+ - 'Error return value of .(.*\.Help|.*\.MarkFlagRequired|(os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*Print(f|ln|)|os\.(Un)?Setenv). is not checked'
+ # Weird error only seen on Kochiku...
+ - 'internal error: no range for'
+ - 'exported method `.*\.(MarshalJSON|UnmarshalJSON|URN|Payload|GoString|Close|Provides|Requires|ExcludeFromHash|MarshalText|UnmarshalText|Description|Check|Poll|Severity)` should have comment or be unexported'
+ - 'composite literal uses unkeyed fields'
+ - 'declaration of "err" shadows declaration'
+ - 'by other packages, and that stutters'
+ - 'Potential file inclusion via variable'
+ - 'at least one file in a package should have a package comment'
+ - 'bad syntax for struct tag pair'
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index c056a1b..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-sudo: false
-language: go
-install: go get -t -v ./...
-go:
- - 1.15
diff --git a/README.md b/README.md
index 78839a4..c40d7b4 100644
--- a/README.md
+++ b/README.md
@@ -1,190 +1 @@
-# Go JSON Schema Reflection
-
-[![Build Status](https://travis-ci.org/alecthomas/jsonschema.png)](https://travis-ci.org/alecthomas/jsonschema)
-[![Gitter chat](https://badges.gitter.im/alecthomas.png)](https://gitter.im/alecthomas/Lobby)
-[![Go Report Card](https://goreportcard.com/badge/github.com/alecthomas/jsonschema)](https://goreportcard.com/report/github.com/alecthomas/jsonschema)
-[![GoDoc](https://godoc.org/github.com/alecthomas/jsonschema?status.svg)](https://godoc.org/github.com/alecthomas/jsonschema)
-
-This package can be used to generate [JSON Schemas](http://json-schema.org/latest/json-schema-validation.html) from Go types through reflection.
-
-- Supports arbitrarily complex types, including `interface{}`, maps, slices, etc.
-- Supports json-schema features such as minLength, maxLength, pattern, format, etc.
-- Supports simple string and numeric enums.
-- Supports custom property fields via the `jsonschema_extras` struct tag.
-
-## Example
-
-The following Go type:
-
-```go
-type TestUser struct {
- ID int `json:"id"`
- Name string `json:"name" jsonschema:"title=the name,description=The name of a friend,example=joe,example=lucy,default=alex"`
- Friends []int `json:"friends,omitempty" jsonschema_description:"The list of IDs, omitted when empty"`
- Tags map[string]interface{} `json:"tags,omitempty" jsonschema_extras:"a=b,foo=bar,foo=bar1"`
- BirthDate time.Time `json:"birth_date,omitempty" jsonschema:"oneof_required=date"`
- YearOfBirth string `json:"year_of_birth,omitempty" jsonschema:"oneof_required=year"`
- Metadata interface{} `json:"metadata,omitempty" jsonschema:"oneof_type=string;array"`
- FavColor string `json:"fav_color,omitempty" jsonschema:"enum=red,enum=green,enum=blue"`
-}
-```
-
-Results in following JSON Schema:
-
-```go
-jsonschema.Reflect(&TestUser{})
-```
-
-```json
-{
- "$schema": "http://json-schema.org/draft-04/schema#",
- "$ref": "#/definitions/TestUser",
- "definitions": {
- "TestUser": {
- "type": "object",
- "properties": {
- "metadata": {
- "oneOf": [
- {
- "type": "string"
- },
- {
- "type": "array"
- }
- ]
- },
- "birth_date": {
- "type": "string",
- "format": "date-time"
- },
- "friends": {
- "type": "array",
- "items": {
- "type": "integer"
- },
- "description": "The list of IDs, omitted when empty"
- },
- "id": {
- "type": "integer"
- },
- "name": {
- "type": "string",
- "title": "the name",
- "description": "The name of a friend",
- "default": "alex",
- "examples": [
- "joe",
- "lucy"
- ]
- },
- "tags": {
- "type": "object",
- "patternProperties": {
- ".*": {
- "additionalProperties": true
- }
- },
- "a": "b",
- "foo": [
- "bar",
- "bar1"
- ]
- },
- "fav_color": {
- "type": "string",
- "enum": [
- "red",
- "green",
- "blue"
- ]
- }
- },
- "additionalProperties": false,
- "required": ["id", "name"],
- "oneOf": [
- {
- "required": [
- "birth_date"
- ],
- "title": "date"
- },
- {
- "required": [
- "year_of_birth"
- ],
- "title": "year"
- }
- ]
- }
- }
-}
-```
-## Configurable behaviour
-
-The behaviour of the schema generator can be altered with parameters when a `jsonschema.Reflector`
-instance is created.
-
-### ExpandedStruct
-
-If set to ```true```, makes the top level struct not to reference itself in the definitions. But type passed should be a struct type.
-
-eg.
-
-```go
-type GrandfatherType struct {
- FamilyName string `json:"family_name" jsonschema:"required"`
-}
-
-type SomeBaseType struct {
- SomeBaseProperty int `json:"some_base_property"`
- // The jsonschema required tag is nonsensical for private and ignored properties.
- // Their presence here tests that the fields *will not* be required in the output
- // schema, even if they are tagged required.
- somePrivateBaseProperty string `json:"i_am_private" jsonschema:"required"`
- SomeIgnoredBaseProperty string `json:"-" jsonschema:"required"`
- SomeSchemaIgnoredProperty string `jsonschema:"-,required"`
- SomeUntaggedBaseProperty bool `jsonschema:"required"`
- someUnexportedUntaggedBaseProperty bool
- Grandfather GrandfatherType `json:"grand"`
-}
-```
-
-will output:
-
-```json
-{
- "$schema": "http://json-schema.org/draft-04/schema#",
- "required": [
- "some_base_property",
- "grand",
- "SomeUntaggedBaseProperty"
- ],
- "properties": {
- "SomeUntaggedBaseProperty": {
- "type": "boolean"
- },
- "grand": {
- "$schema": "http://json-schema.org/draft-04/schema#",
- "$ref": "#/definitions/GrandfatherType"
- },
- "some_base_property": {
- "type": "integer"
- }
- },
- "type": "object",
- "definitions": {
- "GrandfatherType": {
- "required": [
- "family_name"
- ],
- "properties": {
- "family_name": {
- "type": "string"
- }
- },
- "additionalProperties": false,
- "type": "object"
- }
- }
-}
-```
+# Maintenance of this project has moved to [invopop/jsonschema](https://github.com/invopop/jsonschema).
diff --git a/bin/.go@latest.pkg b/bin/.go@latest.pkg
new file mode 120000
index 0000000..383f451
--- /dev/null
+++ b/bin/.go@latest.pkg
@@ -0,0 +1 @@
+hermit
\ No newline at end of file
diff --git a/bin/.golangci-lint-1.42.1.pkg b/bin/.golangci-lint-1.42.1.pkg
new file mode 120000
index 0000000..383f451
--- /dev/null
+++ b/bin/.golangci-lint-1.42.1.pkg
@@ -0,0 +1 @@
+hermit
\ No newline at end of file
diff --git a/bin/README.hermit.md b/bin/README.hermit.md
new file mode 100644
index 0000000..e889550
--- /dev/null
+++ b/bin/README.hermit.md
@@ -0,0 +1,7 @@
+# Hermit environment
+
+This is a [Hermit](https://github.com/cashapp/hermit) bin directory.
+
+The symlinks in this directory are managed by Hermit and will automatically
+download and install Hermit itself as well as packages. These packages are
+local to this environment.
diff --git a/bin/activate-hermit b/bin/activate-hermit
new file mode 100755
index 0000000..3b191fb
--- /dev/null
+++ b/bin/activate-hermit
@@ -0,0 +1,19 @@
+#!/bin/bash
+# This file must be used with "source bin/activate-hermit" from bash or zsh.
+# You cannot run it directly
+
+if [ "${BASH_SOURCE-}" = "$0" ]; then
+ echo "You must source this script: \$ source $0" >&2
+ exit 33
+fi
+
+BIN_DIR="$(dirname "${BASH_SOURCE[0]:-${(%):-%x}}")"
+if "${BIN_DIR}/hermit" noop > /dev/null; then
+ eval "$("${BIN_DIR}/hermit" activate "${BIN_DIR}/..")"
+
+ if [ -n "${BASH-}" ] || [ -n "${ZSH_VERSION-}" ]; then
+ hash -r 2>/dev/null
+ fi
+
+ echo "Hermit environment $("${HERMIT_ENV}"/bin/hermit env HERMIT_ENV) activated"
+fi
diff --git a/bin/go b/bin/go
new file mode 120000
index 0000000..142d6ff
--- /dev/null
+++ b/bin/go
@@ -0,0 +1 @@
+.go@latest.pkg
\ No newline at end of file
diff --git a/bin/gofmt b/bin/gofmt
new file mode 120000
index 0000000..142d6ff
--- /dev/null
+++ b/bin/gofmt
@@ -0,0 +1 @@
+.go@latest.pkg
\ No newline at end of file
diff --git a/bin/golangci-lint b/bin/golangci-lint
new file mode 120000
index 0000000..ae19155
--- /dev/null
+++ b/bin/golangci-lint
@@ -0,0 +1 @@
+.golangci-lint-1.42.1.pkg
\ No newline at end of file
diff --git a/bin/hermit b/bin/hermit
new file mode 100755
index 0000000..d13a24d
--- /dev/null
+++ b/bin/hermit
@@ -0,0 +1,26 @@
+#!/bin/bash
+
+set -eo pipefail
+
+if [ -z "${HERMIT_STATE_DIR}" ]; then
+ case "$(uname -s)" in
+ Darwin)
+ export HERMIT_STATE_DIR="${HOME}/Library/Caches/hermit"
+ ;;
+ Linux)
+ export HERMIT_STATE_DIR="${XDG_CACHE_HOME:-${HOME}/.cache}/hermit"
+ ;;
+ esac
+fi
+
+export HERMIT_DIST_URL="${HERMIT_DIST_URL:-https://github.com/cashapp/hermit/releases/download/stable}"
+HERMIT_CHANNEL="$(basename "${HERMIT_DIST_URL}")"
+export HERMIT_CHANNEL
+export HERMIT_EXE=${HERMIT_EXE:-${HERMIT_STATE_DIR}/pkg/hermit@${HERMIT_CHANNEL}/hermit}
+
+if [ ! -x "${HERMIT_EXE}" ]; then
+ echo "Bootstrapping ${HERMIT_EXE} from ${HERMIT_DIST_URL}" 1>&2
+ curl -fsSL "${HERMIT_DIST_URL}/install.sh" | /bin/bash 1>&2
+fi
+
+exec "${HERMIT_EXE}" --level=fatal exec "$0" -- "$@"
diff --git a/bin/hermit.hcl b/bin/hermit.hcl
new file mode 100644
index 0000000..e69de29
diff --git a/comment_extractor.go b/comment_extractor.go
new file mode 100644
index 0000000..0088b41
--- /dev/null
+++ b/comment_extractor.go
@@ -0,0 +1,90 @@
+package jsonschema
+
+import (
+ "fmt"
+ "io/fs"
+ gopath "path"
+ "path/filepath"
+ "strings"
+
+ "go/ast"
+ "go/doc"
+ "go/parser"
+ "go/token"
+)
+
+// ExtractGoComments will read all the go files contained in the provided path,
+// including sub-directories, in order to generate a dictionary of comments
+// associated with Types and Fields. The results will be added to the `commentsMap`
+// provided in the parameters and expected to be used for Schema "description" fields.
+//
+// The `go/parser` library is used to extract all the comments and unfortunately doesn't
+// have a built-in way to determine the fully qualified name of a package. The `base` paremeter,
+// the URL used to import that package, is thus required to be able to match reflected types.
+//
+// When parsing type comments, we use the `go/doc`'s Synopsis method to extract the first phrase
+// only. Field comments, which tend to be much shorter, will include everything.
+func ExtractGoComments(base, path string, commentMap map[string]string) error {
+ fset := token.NewFileSet()
+ dict := make(map[string][]*ast.Package)
+ err := filepath.Walk(path, func(path string, info fs.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ if info.IsDir() {
+ d, err := parser.ParseDir(fset, path, nil, parser.ParseComments)
+ if err != nil {
+ return err
+ }
+ for _, v := range d {
+ // paths may have multiple packages, like for tests
+ k := gopath.Join(base, path)
+ dict[k] = append(dict[k], v)
+ }
+ }
+ return nil
+ })
+ if err != nil {
+ return err
+ }
+
+ for pkg, p := range dict {
+ for _, f := range p {
+ gtxt := ""
+ typ := ""
+ ast.Inspect(f, func(n ast.Node) bool {
+ switch x := n.(type) {
+ case *ast.TypeSpec:
+ typ = x.Name.String()
+ if !ast.IsExported(typ) {
+ typ = ""
+ } else {
+ txt := x.Doc.Text()
+ if txt == "" && gtxt != "" {
+ txt = gtxt
+ gtxt = ""
+ }
+ txt = doc.Synopsis(txt)
+ commentMap[fmt.Sprintf("%s.%s", pkg, typ)] = strings.TrimSpace(txt)
+ }
+ case *ast.Field:
+ txt := x.Doc.Text()
+ if typ != "" && txt != "" {
+ for _, n := range x.Names {
+ if ast.IsExported(n.String()) {
+ k := fmt.Sprintf("%s.%s.%s", pkg, typ, n)
+ commentMap[k] = strings.TrimSpace(txt)
+ }
+ }
+ }
+ case *ast.GenDecl:
+ // remember for the next type
+ gtxt = x.Doc.Text()
+ }
+ return true
+ })
+ }
+ }
+
+ return nil
+}
diff --git a/debian/changelog b/debian/changelog
index d6e50dd..51b0fc9 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+golang-github-alecthomas-jsonschema (0.0~git20220216.9eeeec9-1) UNRELEASED; urgency=low
+
+ * New upstream snapshot.
+
+ -- Debian Janitor <janitor@jelmer.uk> Thu, 19 Jan 2023 07:00:55 -0000
+
golang-github-alecthomas-jsonschema (0.0~git20210127.19bc6f2-2) unstable; urgency=medium
[ Debian Janitor ]
diff --git a/examples/nested/nested.go b/examples/nested/nested.go
new file mode 100644
index 0000000..953c499
--- /dev/null
+++ b/examples/nested/nested.go
@@ -0,0 +1,15 @@
+package nested
+
+// Pet defines the user's fury friend.
+type Pet struct {
+ // Name of the animal.
+ Name string `json:"name" jsonschema:"title=Name"`
+}
+
+type (
+ // Plant represents the plants the user might have and serves as a test
+ // of structs inside a `type` set.
+ Plant struct {
+ Variant string `json:"variant" jsonschema:"title=Variant"` // This comment will be ignored
+ }
+)
diff --git a/examples/user.go b/examples/user.go
new file mode 100644
index 0000000..12ac497
--- /dev/null
+++ b/examples/user.go
@@ -0,0 +1,22 @@
+package examples
+
+import (
+ "github.com/alecthomas/jsonschema/examples/nested"
+)
+
+// User is used as a base to provide tests for comments.
+// Don't forget to checkout the nested path.
+type User struct {
+ // Unique sequential identifier.
+ ID int `json:"id" jsonschema:"required"`
+ // This comment will be ignored
+ Name string `json:"name" jsonschema:"required,minLength=1,maxLength=20,pattern=.*,description=this is a property,title=the name,example=joe,example=lucy,default=alex"`
+ Friends []int `json:"friends,omitempty" jsonschema_description:"list of IDs, omitted when empty"`
+ Tags map[string]interface{} `json:"tags,omitempty"`
+
+ // An array of pets the user cares for.
+ Pets []*nested.Pet `json:"pets"`
+
+ // Set of plants that the user likes
+ Plants []*nested.Plant `json:"plants" jsonschema:"title=Pants"`
+}
diff --git a/fixtures/allow_additional_props.json b/fixtures/allow_additional_props.json
index 623e5ca..febe259 100644
--- a/fixtures/allow_additional_props.json
+++ b/fixtures/allow_additional_props.json
@@ -23,11 +23,14 @@
"PublicNonExported",
"id",
"name",
+ "password",
"TestFlag",
"age",
"email",
"Baz",
- "color"
+ "color",
+ "roles",
+ "raw"
],
"properties": {
"some_base_property": {
@@ -60,7 +63,12 @@
"examples": [
"joe",
"lucy"
- ]
+ ],
+ "readOnly": true
+ },
+ "password": {
+ "type": "string",
+ "writeOnly": true
},
"friends": {
"items": {
@@ -114,7 +122,7 @@
}
]
},
- "age":{
+ "age": {
"maximum": 120,
"exclusiveMaximum": true,
"minimum": 18,
@@ -156,10 +164,46 @@
2
],
"type": "number"
+ },
+ "roles": {
+ "items": {
+ "enum": [
+ "admin",
+ "moderator",
+ "user"
+ ],
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "priorities": {
+ "items": {
+ "enum": [
+ -1,
+ 0,
+ 1
+ ],
+ "type": "integer"
+ },
+ "type": "array"
+ },
+ "offsets": {
+ "items": {
+ "enum": [
+ 1.570796,
+ 3.141592,
+ 6.283185
+ ],
+ "type": "number"
+ },
+ "type": "array"
+ },
+ "raw": {
+ "additionalProperties": true
}
},
"additionalProperties": true,
"type": "object"
}
}
-}
+}
\ No newline at end of file
diff --git a/fixtures/compact_date.json b/fixtures/compact_date.json
new file mode 100644
index 0000000..478187d
--- /dev/null
+++ b/fixtures/compact_date.json
@@ -0,0 +1,12 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "$ref": "#/definitions/CompactDate",
+ "definitions": {
+ "CompactDate": {
+ "pattern": "^[0-9]{4}-[0-1][0-9]$",
+ "type": "string",
+ "title": "Compact Date",
+ "description": "Short date that only includes year and month"
+ }
+ }
+}
\ No newline at end of file
diff --git a/fixtures/custom_additional.json b/fixtures/custom_additional.json
new file mode 100644
index 0000000..b3a841c
--- /dev/null
+++ b/fixtures/custom_additional.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "http:\/\/json-schema.org\/draft-04\/schema#",
+ "$ref": "#\/definitions\/GrandfatherType",
+ "definitions": {
+ "GrandfatherType": {
+ "required": [
+ "family_name",
+ "ip_addr"
+ ],
+ "properties": {
+ "family_name": {
+ "type": "string"
+ },
+ "ip_addr": {
+ "type": "string",
+ "format": "ipv4"
+ }
+ },
+ "additionalProperties": false,
+ "type": "object"
+ }
+ }
+}
diff --git a/fixtures/custom_map_type.json b/fixtures/custom_map_type.json
new file mode 100644
index 0000000..7d6f188
--- /dev/null
+++ b/fixtures/custom_map_type.json
@@ -0,0 +1,37 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "$ref": "#/definitions/CustomMapOuter",
+ "definitions": {
+ "CustomMapOuter": {
+ "type": "object",
+ "required": [
+ "my_map"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "my_map": {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "$ref": "#/definitions/CustomMapType"
+ }
+ }
+ },
+ "CustomMapType": {
+ "items": {
+ "required": [
+ "key",
+ "value"
+ ],
+ "properties": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ }
+ }
+}
diff --git a/fixtures/custom_slice_type.json b/fixtures/custom_slice_type.json
new file mode 100644
index 0000000..dee08f4
--- /dev/null
+++ b/fixtures/custom_slice_type.json
@@ -0,0 +1,32 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "$ref": "#/definitions/CustomSliceOuter",
+ "definitions": {
+ "CustomSliceOuter": {
+ "type": "object",
+ "required": [
+ "slice"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "slice": {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "$ref": "#/definitions/CustomSliceType"
+ }
+ }
+ },
+ "CustomSliceType": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ }
+ ]
+ }
+ }
+}
diff --git a/fixtures/custom_type_with_interface.json b/fixtures/custom_type_with_interface.json
new file mode 100644
index 0000000..8e8d275
--- /dev/null
+++ b/fixtures/custom_type_with_interface.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "$ref": "#/definitions/CustomTypeFieldWithInterface",
+ "definitions": {
+ "CustomTypeFieldWithInterface": {
+ "required": [
+ "CreatedAt"
+ ],
+ "properties": {
+ "CreatedAt": {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "$ref": "#/definitions/CustomTimeWithInterface"
+ }
+ },
+ "additionalProperties": false,
+ "type": "object"
+ },
+ "CustomTimeWithInterface": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+}
diff --git a/fixtures/defaults.json b/fixtures/defaults.json
index b3a4c9a..f2fcfa9 100644
--- a/fixtures/defaults.json
+++ b/fixtures/defaults.json
@@ -23,11 +23,14 @@
"PublicNonExported",
"id",
"name",
+ "password",
"TestFlag",
"age",
"email",
"Baz",
- "color"
+ "color",
+ "roles",
+ "raw"
],
"properties": {
"some_base_property": {
@@ -60,7 +63,12 @@
"examples": [
"joe",
"lucy"
- ]
+ ],
+ "readOnly": true
+ },
+ "password": {
+ "type": "string",
+ "writeOnly": true
},
"friends": {
"items": {
@@ -156,6 +164,42 @@
2
],
"type": "number"
+ },
+ "roles": {
+ "items": {
+ "enum": [
+ "admin",
+ "moderator",
+ "user"
+ ],
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "priorities": {
+ "items": {
+ "enum": [
+ -1,
+ 0,
+ 1
+ ],
+ "type": "integer"
+ },
+ "type": "array"
+ },
+ "offsets": {
+ "items": {
+ "enum": [
+ 1.570796,
+ 3.141592,
+ 6.283185
+ ],
+ "type": "number"
+ },
+ "type": "array"
+ },
+ "raw": {
+ "additionalProperties": true
}
},
"additionalProperties": false,
diff --git a/fixtures/defaults_expanded_toplevel.json b/fixtures/defaults_expanded_toplevel.json
index 4bb3bac..859e690 100644
--- a/fixtures/defaults_expanded_toplevel.json
+++ b/fixtures/defaults_expanded_toplevel.json
@@ -8,11 +8,14 @@
"PublicNonExported",
"id",
"name",
+ "password",
"TestFlag",
"age",
"email",
"Baz",
- "color"
+ "color",
+ "roles",
+ "raw"
],
"properties": {
"some_base_property": {
@@ -35,17 +38,22 @@
"type": "integer"
},
"name": {
- "maxLength": 20,
- "minLength": 1,
- "pattern": ".*",
- "type": "string",
- "title": "the name",
- "description": "this is a property",
- "default": "alex",
- "examples": [
- "joe",
- "lucy"
- ]
+ "maxLength": 20,
+ "minLength": 1,
+ "pattern": ".*",
+ "type": "string",
+ "title": "the name",
+ "description": "this is a property",
+ "default": "alex",
+ "examples": [
+ "joe",
+ "lucy"
+ ],
+ "readOnly": true
+ },
+ "password": {
+ "type": "string",
+ "writeOnly": true
},
"friends": {
"items": {
@@ -99,7 +107,7 @@
}
]
},
- "age":{
+ "age": {
"maximum": 120,
"exclusiveMaximum": true,
"minimum": 18,
@@ -112,10 +120,10 @@
},
"Baz": {
"type": "string",
- "foo": [
- "bar",
- "bar1"
- ],
+ "foo": [
+ "bar",
+ "bar1"
+ ],
"hello": "world"
},
"color": {
@@ -141,6 +149,42 @@
2.0
],
"type": "number"
+ },
+ "roles": {
+ "items": {
+ "enum": [
+ "admin",
+ "moderator",
+ "user"
+ ],
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "priorities": {
+ "items": {
+ "enum": [
+ -1,
+ 0,
+ 1
+ ],
+ "type": "integer"
+ },
+ "type": "array"
+ },
+ "offsets": {
+ "items": {
+ "enum": [
+ 1.570796,
+ 3.141592,
+ 6.283185
+ ],
+ "type": "number"
+ },
+ "type": "array"
+ },
+ "raw": {
+ "additionalProperties": true
}
},
"additionalProperties": false,
@@ -159,4 +203,4 @@
"type": "object"
}
}
-}
+}
\ No newline at end of file
diff --git a/fixtures/fully_qualified.json b/fixtures/fully_qualified.json
index 9b3041d..1c1a303 100644
--- a/fixtures/fully_qualified.json
+++ b/fixtures/fully_qualified.json
@@ -23,11 +23,14 @@
"PublicNonExported",
"id",
"name",
+ "password",
"TestFlag",
"age",
"email",
"Baz",
- "color"
+ "color",
+ "roles",
+ "raw"
],
"properties": {
"some_base_property": {
@@ -60,7 +63,12 @@
"examples": [
"joe",
"lucy"
- ]
+ ],
+ "readOnly": true
+ },
+ "password": {
+ "type": "string",
+ "writeOnly": true
},
"friends": {
"items": {
@@ -156,6 +164,42 @@
2
],
"type": "number"
+ },
+ "roles": {
+ "items": {
+ "enum": [
+ "admin",
+ "moderator",
+ "user"
+ ],
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "priorities": {
+ "items": {
+ "enum": [
+ -1,
+ 0,
+ 1
+ ],
+ "type": "integer"
+ },
+ "type": "array"
+ },
+ "offsets": {
+ "items": {
+ "enum": [
+ 1.570796,
+ 3.141592,
+ 6.283185
+ ],
+ "type": "number"
+ },
+ "type": "array"
+ },
+ "raw": {
+ "additionalProperties": true
}
},
"additionalProperties": false,
diff --git a/fixtures/go_comments.json b/fixtures/go_comments.json
new file mode 100644
index 0000000..dc88065
--- /dev/null
+++ b/fixtures/go_comments.json
@@ -0,0 +1,97 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "$ref": "#/definitions/User",
+ "definitions": {
+ "Pet": {
+ "required": [
+ "name"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "title": "Name",
+ "description": "Name of the animal."
+ }
+ },
+ "additionalProperties": false,
+ "type": "object",
+ "description": "Pet defines the user's fury friend."
+ },
+ "Plant": {
+ "required": [
+ "variant"
+ ],
+ "properties": {
+ "variant": {
+ "type": "string",
+ "title": "Variant"
+ }
+ },
+ "additionalProperties": false,
+ "type": "object",
+ "description": "Plant represents the plants the user might have and serves as a test of structs inside a `type` set."
+ },
+ "User": {
+ "required": [
+ "id",
+ "name",
+ "pets",
+ "plants"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer",
+ "description": "Unique sequential identifier."
+ },
+ "name": {
+ "maxLength": 20,
+ "minLength": 1,
+ "pattern": ".*",
+ "type": "string",
+ "title": "the name",
+ "description": "this is a property",
+ "default": "alex",
+ "examples": [
+ "joe",
+ "lucy"
+ ]
+ },
+ "friends": {
+ "items": {
+ "type": "integer"
+ },
+ "type": "array",
+ "description": "list of IDs, omitted when empty"
+ },
+ "tags": {
+ "patternProperties": {
+ ".*": {
+ "additionalProperties": true
+ }
+ },
+ "type": "object"
+ },
+ "pets": {
+ "items": {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "$ref": "#/definitions/Pet"
+ },
+ "type": "array",
+ "description": "An array of pets the user cares for."
+ },
+ "plants": {
+ "items": {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "$ref": "#/definitions/Plant"
+ },
+ "type": "array",
+ "title": "Pants",
+ "description": "Set of plants that the user likes"
+ }
+ },
+ "additionalProperties": false,
+ "type": "object",
+ "description": "User is used as a base to provide tests for comments."
+ }
+ }
+}
\ No newline at end of file
diff --git a/fixtures/ignore_type.json b/fixtures/ignore_type.json
index c36d9fc..b21451a 100644
--- a/fixtures/ignore_type.json
+++ b/fixtures/ignore_type.json
@@ -16,11 +16,14 @@
"PublicNonExported",
"id",
"name",
+ "password",
"TestFlag",
"age",
"email",
"Baz",
- "color"
+ "color",
+ "roles",
+ "raw"
],
"properties": {
"some_base_property": {
@@ -53,7 +56,12 @@
"examples": [
"joe",
"lucy"
- ]
+ ],
+ "readOnly": true
+ },
+ "password": {
+ "type": "string",
+ "writeOnly": true
},
"friends": {
"items": {
@@ -149,10 +157,46 @@
2
],
"type": "number"
+ },
+ "roles": {
+ "items": {
+ "enum": [
+ "admin",
+ "moderator",
+ "user"
+ ],
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "priorities": {
+ "items": {
+ "enum": [
+ -1,
+ 0,
+ 1
+ ],
+ "type": "integer"
+ },
+ "type": "array"
+ },
+ "offsets": {
+ "items": {
+ "enum": [
+ 1.570796,
+ 3.141592,
+ 6.283185
+ ],
+ "type": "number"
+ },
+ "type": "array"
+ },
+ "raw": {
+ "additionalProperties": true
}
},
"additionalProperties": false,
"type": "object"
}
}
-}
+}
\ No newline at end of file
diff --git a/fixtures/no_ref_qual_types.json b/fixtures/no_ref_qual_types.json
index c53688c..85bf476 100644
--- a/fixtures/no_ref_qual_types.json
+++ b/fixtures/no_ref_qual_types.json
@@ -7,11 +7,14 @@
"PublicNonExported",
"id",
"name",
+ "password",
"TestFlag",
"age",
"email",
"Baz",
- "color"
+ "color",
+ "roles",
+ "raw"
],
"properties": {
"some_base_property": {
@@ -52,7 +55,12 @@
"examples": [
"joe",
"lucy"
- ]
+ ],
+ "readOnly": true
+ },
+ "password": {
+ "type": "string",
+ "writeOnly": true
},
"friends": {
"items": {
@@ -148,6 +156,42 @@
2
],
"type": "number"
+ },
+ "roles": {
+ "items": {
+ "enum": [
+ "admin",
+ "moderator",
+ "user"
+ ],
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "priorities": {
+ "items": {
+ "enum": [
+ -1,
+ 0,
+ 1
+ ],
+ "type": "integer"
+ },
+ "type": "array"
+ },
+ "offsets": {
+ "items": {
+ "enum": [
+ 1.570796,
+ 3.141592,
+ 6.283185
+ ],
+ "type": "number"
+ },
+ "type": "array"
+ },
+ "raw": {
+ "additionalProperties": true
}
},
"additionalProperties": false,
@@ -174,11 +218,14 @@
"PublicNonExported",
"id",
"name",
+ "password",
"TestFlag",
"age",
"email",
"Baz",
- "color"
+ "color",
+ "roles",
+ "raw"
],
"properties": {
"some_base_property": {
@@ -219,7 +266,12 @@
"examples": [
"joe",
"lucy"
- ]
+ ],
+ "readOnly": true
+ },
+ "password": {
+ "type": "string",
+ "writeOnly": true
},
"friends": {
"items": {
@@ -315,6 +367,42 @@
2
],
"type": "number"
+ },
+ "roles": {
+ "items": {
+ "enum": [
+ "admin",
+ "moderator",
+ "user"
+ ],
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "priorities": {
+ "items": {
+ "enum": [
+ -1,
+ 0,
+ 1
+ ],
+ "type": "integer"
+ },
+ "type": "array"
+ },
+ "offsets": {
+ "items": {
+ "enum": [
+ 1.570796,
+ 3.141592,
+ 6.283185
+ ],
+ "type": "number"
+ },
+ "type": "array"
+ },
+ "raw": {
+ "additionalProperties": true
}
},
"additionalProperties": false,
diff --git a/fixtures/no_reference.json b/fixtures/no_reference.json
index ba1f8d0..6a51127 100644
--- a/fixtures/no_reference.json
+++ b/fixtures/no_reference.json
@@ -7,11 +7,14 @@
"PublicNonExported",
"id",
"name",
+ "password",
"TestFlag",
"age",
"email",
"Baz",
- "color"
+ "color",
+ "roles",
+ "raw"
],
"properties": {
"some_base_property": {
@@ -52,7 +55,12 @@
"examples": [
"joe",
"lucy"
- ]
+ ],
+ "readOnly": true
+ },
+ "password": {
+ "type": "string",
+ "writeOnly": true
},
"friends": {
"items": {
@@ -148,6 +156,42 @@
2
],
"type": "number"
+ },
+ "roles": {
+ "items": {
+ "enum": [
+ "admin",
+ "moderator",
+ "user"
+ ],
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "priorities": {
+ "items": {
+ "enum": [
+ -1,
+ 0,
+ 1
+ ],
+ "type": "integer"
+ },
+ "type": "array"
+ },
+ "offsets": {
+ "items": {
+ "enum": [
+ 1.570796,
+ 3.141592,
+ 6.283185
+ ],
+ "type": "number"
+ },
+ "type": "array"
+ },
+ "raw": {
+ "additionalProperties": true
}
},
"additionalProperties": false,
@@ -174,11 +218,14 @@
"PublicNonExported",
"id",
"name",
+ "password",
"TestFlag",
"age",
"email",
"Baz",
- "color"
+ "color",
+ "roles",
+ "raw"
],
"properties": {
"some_base_property": {
@@ -219,7 +266,12 @@
"examples": [
"joe",
"lucy"
- ]
+ ],
+ "readOnly": true
+ },
+ "password": {
+ "type": "string",
+ "writeOnly": true
},
"friends": {
"items": {
@@ -315,6 +367,42 @@
2
],
"type": "number"
+ },
+ "roles": {
+ "items": {
+ "enum": [
+ "admin",
+ "moderator",
+ "user"
+ ],
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "priorities": {
+ "items": {
+ "enum": [
+ -1,
+ 0,
+ 1
+ ],
+ "type": "integer"
+ },
+ "type": "array"
+ },
+ "offsets": {
+ "items": {
+ "enum": [
+ 1.570796,
+ 3.141592,
+ 6.283185
+ ],
+ "type": "number"
+ },
+ "type": "array"
+ },
+ "raw": {
+ "additionalProperties": true
}
},
"additionalProperties": false,
diff --git a/fixtures/required_from_jsontags.json b/fixtures/required_from_jsontags.json
index a27ad19..11fb53f 100644
--- a/fixtures/required_from_jsontags.json
+++ b/fixtures/required_from_jsontags.json
@@ -53,7 +53,12 @@
"examples": [
"joe",
"lucy"
- ]
+ ],
+ "readOnly": true
+ },
+ "password": {
+ "type": "string",
+ "writeOnly": true
},
"friends": {
"items": {
@@ -107,7 +112,7 @@
}
]
},
- "age":{
+ "age": {
"maximum": 120,
"exclusiveMaximum": true,
"minimum": 18,
@@ -149,10 +154,46 @@
2
],
"type": "number"
+ },
+ "roles": {
+ "items": {
+ "enum": [
+ "admin",
+ "moderator",
+ "user"
+ ],
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "priorities": {
+ "items": {
+ "enum": [
+ -1,
+ 0,
+ 1
+ ],
+ "type": "integer"
+ },
+ "type": "array"
+ },
+ "offsets": {
+ "items": {
+ "enum": [
+ 1.570796,
+ 3.141592,
+ 6.283185
+ ],
+ "type": "number"
+ },
+ "type": "array"
+ },
+ "raw": {
+ "additionalProperties": true
}
},
"additionalProperties": false,
"type": "object"
}
}
-}
+}
\ No newline at end of file
diff --git a/fixtures/schema_with_minimum.json b/fixtures/schema_with_minimum.json
new file mode 100644
index 0000000..4522324
--- /dev/null
+++ b/fixtures/schema_with_minimum.json
@@ -0,0 +1,19 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "$ref": "#/definitions/MinValue",
+ "definitions": {
+ "MinValue": {
+ "required": [
+ "value4"
+ ],
+ "properties": {
+ "value4": {
+ "type": "integer",
+ "minimum": 0
+ }
+ },
+ "additionalProperties": false,
+ "type": "object"
+ }
+ }
+ }
\ No newline at end of file
diff --git a/fixtures/test_yaml_and_json.json b/fixtures/test_yaml_and_json.json
new file mode 100644
index 0000000..0999cb6
--- /dev/null
+++ b/fixtures/test_yaml_and_json.json
@@ -0,0 +1,25 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "$ref": "#/definitions/TestYamlAndJson",
+ "definitions": {
+ "TestYamlAndJson": {
+ "required": ["FirstName", "LastName", "age"],
+ "properties": {
+ "FirstName": {
+ "type": "string"
+ },
+ "LastName": {
+ "type": "string"
+ },
+ "age": {
+ "type": "integer"
+ },
+ "MiddleName": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "type": "object"
+ }
+ }
+}
diff --git a/fixtures/test_yaml_and_json2.json b/fixtures/test_yaml_and_json2.json
new file mode 100644
index 0000000..48497e8
--- /dev/null
+++ b/fixtures/test_yaml_and_json2.json
@@ -0,0 +1,29 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "$ref": "#/definitions/TestYamlAndJson2",
+ "definitions": {
+ "TestYamlAndJson2": {
+ "required": ["FirstName", "LastName", "age"],
+ "properties": {
+ "FirstName": {
+ "type": "string",
+ "description": "test2"
+ },
+ "LastName": {
+ "type": "string",
+ "description": "test3"
+ },
+ "age": {
+ "type": "integer",
+ "description": "test4"
+ },
+ "MiddleName": {
+ "type": "string",
+ "description": "test5"
+ }
+ },
+ "additionalProperties": false,
+ "type": "object"
+ }
+ }
+}
diff --git a/fixtures/test_yaml_and_json_prefer_yaml.json b/fixtures/test_yaml_and_json_prefer_yaml.json
new file mode 100644
index 0000000..9133d86
--- /dev/null
+++ b/fixtures/test_yaml_and_json_prefer_yaml.json
@@ -0,0 +1,25 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "$ref": "#/definitions/TestYamlAndJson",
+ "definitions": {
+ "TestYamlAndJson": {
+ "required": ["first_name", "LastName", "age"],
+ "properties": {
+ "first_name": {
+ "type": "string"
+ },
+ "LastName": {
+ "type": "string"
+ },
+ "age": {
+ "type": "integer"
+ },
+ "middle_name": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "type": "object"
+ }
+ }
+}
diff --git a/fixtures/yaml_inline.json b/fixtures/yaml_inline.json
new file mode 100644
index 0000000..46d561d
--- /dev/null
+++ b/fixtures/yaml_inline.json
@@ -0,0 +1,29 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "$ref": "#/definitions/TestYamlInline",
+ "definitions": {
+ "Inner": {
+ "required": ["foo"],
+ "properties": {
+ "foo": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "type": "object"
+ },
+ "TestYamlInline": {
+ "required": [
+ "Inlined"
+ ],
+ "properties": {
+ "Inlined": {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "$ref": "#/definitions/Inner"
+ }
+ },
+ "additionalProperties": false,
+ "type": "object"
+ }
+ }
+}
diff --git a/fixtures/yaml_inline_embed.json b/fixtures/yaml_inline_embed.json
new file mode 100644
index 0000000..4b2f064
--- /dev/null
+++ b/fixtures/yaml_inline_embed.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "$ref": "#/definitions/TestYamlInline",
+ "definitions": {
+ "TestYamlInline": {
+ "required": ["foo"],
+ "properties": {
+ "foo": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "type": "object"
+ }
+ }
+}
diff --git a/reflect.go b/reflect.go
index 64de721..34ca4af 100644
--- a/reflect.go
+++ b/reflect.go
@@ -30,6 +30,24 @@ type Schema struct {
Definitions Definitions
}
+// customSchemaType is used to detect if the type provides it's own
+// custom Schema Type definition to use instead. Very useful for situations
+// where there are custom JSON Marshal and Unmarshal methods.
+type customSchemaType interface {
+ JSONSchemaType() *Type
+}
+
+var customType = reflect.TypeOf((*customSchemaType)(nil)).Elem()
+
+// customSchemaGetFieldDocString
+type customSchemaGetFieldDocString interface {
+ GetFieldDocString(fieldName string) string
+}
+
+type customGetFieldDocString func(fieldName string) string
+
+var customStructGetFieldDocString = reflect.TypeOf((*customSchemaGetFieldDocString)(nil)).Elem()
+
// Type represents a JSON Schema object type.
type Type struct {
// RFC draft-wright-json-schema-00
@@ -69,6 +87,9 @@ type Type struct {
Default interface{} `json:"default,omitempty"` // section 6.2
Format string `json:"format,omitempty"` // section 7
Examples []interface{} `json:"examples,omitempty"` // section 7.4
+ // RFC draft-handrews-json-schema-validation-02, section 9.4
+ ReadOnly bool `json:"readOnly,omitempty"`
+ WriteOnly bool `json:"writeOnly,omitempty"`
// RFC draft-wright-json-schema-hyperschema-00, section 4
Media *Type `json:"media,omitempty"` // section 4.3
BinaryEncoding string `json:"binaryEncoding,omitempty"` // section 4.3
@@ -106,6 +127,10 @@ type Reflector struct {
// used with yaml.Marshal/Unmarshal.
YAMLEmbeddedStructs bool
+ // Prefer yaml: tags over json: tags to generate the schema even if json: tags
+ // are present
+ PreferYAMLSchema bool
+
// ExpandedStruct will cause the toplevel definitions of the schema not
// be referenced itself to a definition.
ExpandedStruct bool
@@ -128,8 +153,30 @@ type Reflector struct {
// switching to just allowing additional properties instead.
IgnoredTypes []interface{}
- // TypeMapper is a function that can be used to map custom Go types to jsconschema types.
+ // TypeMapper is a function that can be used to map custom Go types to jsonschema types.
TypeMapper func(reflect.Type) *Type
+
+ // TypeNamer allows customizing of type names
+ TypeNamer func(reflect.Type) string
+
+ // AdditionalFields allows adding structfields for a given type
+ AdditionalFields func(reflect.Type) []reflect.StructField
+
+ // CommentMap is a dictionary of fully qualified go types and fields to comment
+ // strings that will be used if a description has not already been provided in
+ // the tags. Types and fields are added to the package path using "." as a
+ // separator.
+ //
+ // Type descriptions should be defined like:
+ //
+ // map[string]string{"github.com/alecthomas/jsonschema.Reflector": "A Reflector reflects values into a Schema."}
+ //
+ // And Fields defined as:
+ //
+ // map[string]string{"github.com/alecthomas/jsonschema.Reflector.DoNotReference": "Do not reference definitions."}
+ //
+ // See also: AddGoComments
+ CommentMap map[string]string
}
// Reflect reflects to Schema from a value.
@@ -179,6 +226,9 @@ var (
// Byte slices will be encoded as base64
var byteSliceType = reflect.TypeOf([]byte(nil))
+// Except for json.RawMessage
+var rawMessageType = reflect.TypeOf(json.RawMessage{})
+
// Go code generated from protobuf enum types should fulfil this interface.
type protoEnum interface {
EnumDescriptor() ([]byte, []int)
@@ -192,6 +242,16 @@ func (r *Reflector) reflectTypeToSchema(definitions Definitions, t reflect.Type)
return &Type{Ref: "#/definitions/" + r.typeName(t)}
}
+ if r.TypeMapper != nil {
+ if t := r.TypeMapper(t); t != nil {
+ return t
+ }
+ }
+
+ if rt := r.reflectCustomType(definitions, t); rt != nil {
+ return rt
+ }
+
// jsonpb will marshal protobuf enum options as either strings or integers.
// It will unmarshal either.
if t.Implements(protoEnumType) {
@@ -201,24 +261,16 @@ func (r *Reflector) reflectTypeToSchema(definitions Definitions, t reflect.Type)
}}
}
- if r.TypeMapper != nil {
- if t := r.TypeMapper(t); t != nil {
- return t
- }
- }
-
// Defined format types for JSON Schema Validation
// RFC draft-wright-json-schema-validation-00, section 7.3
// TODO email RFC section 7.3.2, hostname RFC section 7.3.3, uriref RFC section 7.3.7
- switch t {
- case ipType:
+ if t == ipType {
// TODO differentiate ipv4 and ipv6 RFC section 7.3.4, 7.3.5
return &Type{Type: "string", Format: "ipv4"} // ipv4 RFC section 7.3.4
}
switch t.Kind() {
case reflect.Struct:
-
switch t {
case timeType: // date-time RFC section 7.3.1
return &Type{Type: "string", Format: "date-time"}
@@ -229,6 +281,18 @@ func (r *Reflector) reflectTypeToSchema(definitions Definitions, t reflect.Type)
}
case reflect.Map:
+ switch t.Key().Kind() {
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ rt := &Type{
+ Type: "object",
+ PatternProperties: map[string]*Type{
+ "^[0-9]+$": r.reflectTypeToSchema(definitions, t.Elem()),
+ },
+ AdditionalProperties: []byte("false"),
+ }
+ return rt
+ }
+
rt := &Type{
Type: "object",
PatternProperties: map[string]*Type{
@@ -240,6 +304,11 @@ func (r *Reflector) reflectTypeToSchema(definitions Definitions, t reflect.Type)
case reflect.Slice, reflect.Array:
returnType := &Type{}
+ if t == rawMessageType {
+ return &Type{
+ AdditionalProperties: []byte("true"),
+ }
+ }
if t.Kind() == reflect.Array {
returnType.MinItems = t.Len()
returnType.MaxItems = returnType.MinItems
@@ -277,8 +346,35 @@ func (r *Reflector) reflectTypeToSchema(definitions Definitions, t reflect.Type)
panic("unsupported type " + t.String())
}
-// Refects a struct to a JSON Schema type.
+func (r *Reflector) reflectCustomType(definitions Definitions, t reflect.Type) *Type {
+ if t.Kind() == reflect.Ptr {
+ return r.reflectCustomType(definitions, t.Elem())
+ }
+
+ if t.Implements(customType) {
+ v := reflect.New(t)
+ o := v.Interface().(customSchemaType)
+ st := o.JSONSchemaType()
+ definitions[r.typeName(t)] = st
+ if r.DoNotReference {
+ return st
+ } else {
+ return &Type{
+ Version: Version,
+ Ref: "#/definitions/" + r.typeName(t),
+ }
+ }
+ }
+
+ return nil
+}
+
+// Reflects a struct to a JSON Schema type.
func (r *Reflector) reflectStruct(definitions Definitions, t reflect.Type) *Type {
+ if st := r.reflectCustomType(definitions, t); st != nil {
+ return st
+ }
+
for _, ignored := range r.IgnoredTypes {
if reflect.TypeOf(ignored) == t {
st := &Type{
@@ -296,13 +392,14 @@ func (r *Reflector) reflectStruct(definitions Definitions, t reflect.Type) *Type
Ref: "#/definitions/" + r.typeName(t),
}
}
-
}
}
+
st := &Type{
Type: "object",
Properties: orderedmap.New(),
AdditionalProperties: []byte("false"),
+ Description: r.lookupComment(t, ""),
}
if r.AllowAdditionalProperties {
st.AdditionalProperties = []byte("true")
@@ -327,20 +424,33 @@ func (r *Reflector) reflectStructFields(st *Type, definitions Definitions, t ref
if t.Kind() != reflect.Struct {
return
}
- for i := 0; i < t.NumField(); i++ {
- f := t.Field(i)
- name, exist, required, nullable := r.reflectFieldName(f)
+
+ var getFieldDocString customGetFieldDocString
+ if t.Implements(customStructGetFieldDocString) {
+ v := reflect.New(t)
+ o := v.Interface().(customSchemaGetFieldDocString)
+ getFieldDocString = o.GetFieldDocString
+ }
+
+ handleField := func(f reflect.StructField) {
+ name, shouldEmbed, required, nullable := r.reflectFieldName(f)
// if anonymous and exported type should be processed recursively
// current type should inherit properties of anonymous one
if name == "" {
- if f.Anonymous && !exist {
+ if shouldEmbed {
r.reflectStructFields(st, definitions, f.Type)
}
- continue
+ return
}
property := r.reflectTypeToSchema(definitions, f.Type)
property.structKeywordsFromTags(f, st, name)
+ if property.Description == "" {
+ property.Description = r.lookupComment(t, f.Name)
+ }
+ if getFieldDocString != nil {
+ property.Description = getFieldDocString(f.Name)
+ }
if nullable {
property = &Type{
@@ -358,6 +468,31 @@ func (r *Reflector) reflectStructFields(st *Type, definitions Definitions, t ref
st.Required = append(st.Required, name)
}
}
+
+ for i := 0; i < t.NumField(); i++ {
+ f := t.Field(i)
+ handleField(f)
+ }
+ if r.AdditionalFields != nil {
+ if af := r.AdditionalFields(t); af != nil {
+ for _, sf := range af {
+ handleField(sf)
+ }
+ }
+ }
+}
+
+func (r *Reflector) lookupComment(t reflect.Type, name string) string {
+ if r.CommentMap == nil {
+ return ""
+ }
+
+ n := fullyQualifiedTypeName(t)
+ if name != "" {
+ n = n + "." + name
+ }
+
+ return r.CommentMap[n]
}
func (t *Type) structKeywordsFromTags(f reflect.StructField, parentType *Type, propertyName string) {
@@ -454,6 +589,12 @@ func (t *Type) stringKeywords(tags []string) {
t.Format = val
break
}
+ case "readOnly":
+ i, _ := strconv.ParseBool(val)
+ t.ReadOnly = i
+ case "writeOnly":
+ i, _ := strconv.ParseBool(val)
+ t.WriteOnly = i
case "default":
t.Default = val
case "example":
@@ -531,6 +672,17 @@ func (t *Type) arrayKeywords(tags []string) {
t.UniqueItems = true
case "default":
defaultValues = append(defaultValues, val)
+ case "enum":
+ switch t.Items.Type {
+ case "string":
+ t.Items.Enum = append(t.Items.Enum, val)
+ case "integer":
+ i, _ := strconv.Atoi(val)
+ t.Items.Enum = append(t.Items.Enum, i)
+ case "number":
+ f, _ := strconv.ParseFloat(val, 64)
+ t.Items.Enum = append(t.Items.Enum, f)
+ }
}
}
}
@@ -553,14 +705,21 @@ func (t *Type) setExtra(key, val string) {
t.Extras = map[string]interface{}{}
}
if existingVal, ok := t.Extras[key]; ok {
- switch existingVal.(type) {
+ switch existingVal := existingVal.(type) {
case string:
- t.Extras[key] = []string{existingVal.(string), val}
+ t.Extras[key] = []string{existingVal, val}
case []string:
- t.Extras[key] = append(existingVal.([]string), val)
+ t.Extras[key] = append(existingVal, val)
+ case int:
+ t.Extras[key], _ = strconv.Atoi(val)
}
} else {
- t.Extras[key] = val
+ switch key {
+ case "minimum":
+ t.Extras[key], _ = strconv.Atoi(val)
+ default:
+ t.Extras[key] = val
+ }
}
}
@@ -601,6 +760,15 @@ func nullableFromJSONSchemaTags(tags []string) bool {
return false
}
+func inlineYAMLTags(tags []string) bool {
+ for _, tag := range tags {
+ if tag == "inline" {
+ return true
+ }
+ }
+ return false
+}
+
func ignoredByJSONTags(tags []string) bool {
return tags[0] == "-"
}
@@ -611,19 +779,22 @@ func ignoredByJSONSchemaTags(tags []string) bool {
func (r *Reflector) reflectFieldName(f reflect.StructField) (string, bool, bool, bool) {
jsonTags, exist := f.Tag.Lookup("json")
- if !exist {
- jsonTags = f.Tag.Get("yaml")
+ yamlTags, yamlExist := f.Tag.Lookup("yaml")
+ if !exist || r.PreferYAMLSchema {
+ jsonTags = yamlTags
+ exist = yamlExist
}
jsonTagsList := strings.Split(jsonTags, ",")
+ yamlTagsList := strings.Split(yamlTags, ",")
if ignoredByJSONTags(jsonTagsList) {
- return "", exist, false, false
+ return "", false, false, false
}
jsonSchemaTags := strings.Split(f.Tag.Get("jsonschema"), ",")
if ignoredByJSONSchemaTags(jsonSchemaTags) {
- return "", exist, false, false
+ return "", false, false, false
}
name := f.Name
@@ -644,16 +815,24 @@ func (r *Reflector) reflectFieldName(f reflect.StructField) (string, bool, bool,
name = ""
}
+ embed := false
+
// field anonymous but without json tag should be inherited by current type
if f.Anonymous && !exist {
if !r.YAMLEmbeddedStructs {
name = ""
+ embed = true
} else {
name = strings.ToLower(name)
}
}
- return name, exist, required, nullable
+ if yamlExist && inlineYAMLTags(yamlTagsList) {
+ name = ""
+ embed = true
+ }
+
+ return name, embed, required, nullable
}
func (s *Schema) MarshalJSON() ([]byte, error) {
@@ -700,8 +879,27 @@ func (t *Type) MarshalJSON() ([]byte, error) {
}
func (r *Reflector) typeName(t reflect.Type) string {
+ if r.TypeNamer != nil {
+ if name := r.TypeNamer(t); name != "" {
+ return name
+ }
+ }
if r.FullyQualifyTypeNames {
- return t.PkgPath() + "." + t.Name()
+ return fullyQualifiedTypeName(t)
}
return t.Name()
}
+
+func fullyQualifiedTypeName(t reflect.Type) string {
+ return t.PkgPath() + "." + t.Name()
+}
+
+// AddGoComments will update the reflectors comment map with all the comments
+// found in the provided source directories. See the #ExtractGoComments method
+// for more details.
+func (r *Reflector) AddGoComments(base, path string) error {
+ if r.CommentMap == nil {
+ r.CommentMap = make(map[string]string)
+ }
+ return ExtractGoComments(base, path, r.CommentMap)
+}
diff --git a/reflect_test.go b/reflect_test.go
index ad3c8a5..e6733d6 100644
--- a/reflect_test.go
+++ b/reflect_test.go
@@ -11,6 +11,10 @@ import (
"testing"
"time"
+ "github.com/iancoleman/orderedmap"
+
+ "github.com/alecthomas/jsonschema/examples"
+
"github.com/stretchr/testify/require"
)
@@ -24,7 +28,7 @@ type SomeBaseType struct {
// The jsonschema required tag is nonsensical for private and ignored properties.
// Their presence here tests that the fields *will not* be required in the output
// schema, even if they are tagged required.
- somePrivateBaseProperty string `json:"i_am_private" jsonschema:"required"`
+ somePrivateBaseProperty string `jsonschema:"required"`
SomeIgnoredBaseProperty string `json:"-" jsonschema:"required"`
SomeSchemaIgnoredProperty string `jsonschema:"-,required"`
Grandfather GrandfatherType `json:"grand"`
@@ -54,10 +58,11 @@ type TestUser struct {
nonExported
MapType
- ID int `json:"id" jsonschema:"required"`
- Name string `json:"name" jsonschema:"required,minLength=1,maxLength=20,pattern=.*,description=this is a property,title=the name,example=joe,example=lucy,default=alex"`
- Friends []int `json:"friends,omitempty" jsonschema_description:"list of IDs, omitted when empty"`
- Tags map[string]interface{} `json:"tags,omitempty"`
+ ID int `json:"id" jsonschema:"required"`
+ Name string `json:"name" jsonschema:"required,minLength=1,maxLength=20,pattern=.*,description=this is a property,title=the name,example=joe,example=lucy,default=alex,readOnly=true"`
+ Password string `json:"password" jsonschema:"writeOnly=true"`
+ Friends []int `json:"friends,omitempty" jsonschema_description:"list of IDs, omitted when empty"`
+ Tags map[string]interface{} `json:"tags,omitempty"`
TestFlag bool
IgnoredCounter int `json:"-"`
@@ -83,6 +88,14 @@ type TestUser struct {
Color string `json:"color" jsonschema:"enum=red,enum=green,enum=blue"`
Rank int `json:"rank,omitempty" jsonschema:"enum=1,enum=2,enum=3"`
Multiplier float64 `json:"mult,omitempty" jsonschema:"enum=1.0,enum=1.5,enum=2.0"`
+
+ // Tests for enum tags on slices
+ Roles []string `json:"roles" jsonschema:"enum=admin,enum=moderator,enum=user"`
+ Priorities []int `json:"priorities,omitempty" jsonschema:"enum=-1,enum=0,enum=1,enun=2"`
+ Offsets []float64 `json:"offsets,omitempty" jsonschema:"enum=1.570796,enum=3.141592,enum=6.283185"`
+
+ // Test for raw JSON
+ Raw json.RawMessage `json:"raw"`
}
type CustomTime time.Time
@@ -91,6 +104,19 @@ type CustomTypeField struct {
CreatedAt CustomTime
}
+type CustomTimeWithInterface time.Time
+
+type CustomTypeFieldWithInterface struct {
+ CreatedAt CustomTimeWithInterface
+}
+
+func (CustomTimeWithInterface) JSONSchemaType() *Type {
+ return &Type{
+ Type: "string",
+ Format: "date-time",
+ }
+}
+
type RootOneOf struct {
Field1 string `json:"field1" jsonschema:"oneof_required=group1"`
Field2 string `json:"field2" jsonschema:"oneof_required=group2"`
@@ -114,12 +140,105 @@ type Inner struct {
Foo string `yaml:"foo"`
}
+type MinValue struct {
+ Value int `json:"value4" jsonschema_extras:"minimum=0"`
+}
type Bytes []byte
type TestNullable struct {
Child1 string `json:"child1" jsonschema:"nullable"`
}
+type TestYamlInline struct {
+ Inlined Inner `yaml:",inline"`
+}
+
+type TestYamlAndJson struct {
+ FirstName string `json:"FirstName" yaml:"first_name"`
+ LastName string `json:"LastName"`
+ Age uint `yaml:"age"`
+ MiddleName string `yaml:"middle_name,omitempty" json:"MiddleName,omitempty"`
+}
+
+type CompactDate struct {
+ Year int
+ Month int
+}
+
+func (CompactDate) JSONSchemaType() *Type {
+ return &Type{
+ Type: "string",
+ Title: "Compact Date",
+ Description: "Short date that only includes year and month",
+ Pattern: "^[0-9]{4}-[0-1][0-9]$",
+ }
+}
+
+type TestYamlAndJson2 struct {
+ FirstName string `json:"FirstName" yaml:"first_name"`
+ LastName string `json:"LastName"`
+ Age uint `yaml:"age"`
+ MiddleName string `yaml:"middle_name,omitempty" json:"MiddleName,omitempty"`
+}
+
+func (TestYamlAndJson2) GetFieldDocString(fieldName string) string {
+ switch fieldName {
+ case "FirstName":
+ return "test2"
+ case "LastName":
+ return "test3"
+ case "Age":
+ return "test4"
+ case "MiddleName":
+ return "test5"
+ default:
+ return ""
+ }
+}
+
+type CustomSliceOuter struct {
+ Slice CustomSliceType `json:"slice"`
+}
+
+type CustomSliceType []string
+
+func (CustomSliceType) JSONSchemaType() *Type {
+ return &Type{
+ OneOf: []*Type{{
+ Type: "string",
+ }, {
+ Type: "array",
+ Items: &Type{
+ Type: "string",
+ },
+ }},
+ }
+}
+
+type CustomMapType map[string]string
+
+func (CustomMapType) JSONSchemaType() *Type {
+ properties := orderedmap.New()
+ properties.Set("key", &Type{
+ Type: "string",
+ })
+ properties.Set("value", &Type{
+ Type: "string",
+ })
+ return &Type{
+ Type: "array",
+ Items: &Type{
+ Type: "object",
+ Properties: properties,
+ Required: []string{"key", "value"},
+ },
+ }
+}
+
+type CustomMapOuter struct {
+ MyMap CustomMapType `json:"my_map"`
+}
+
func TestSchemaGeneration(t *testing.T) {
tests := []struct {
typ interface{}
@@ -148,7 +267,30 @@ func TestSchemaGeneration(t *testing.T) {
}, "fixtures/custom_type.json"},
{&TestUser{}, &Reflector{DoNotReference: true, FullyQualifyTypeNames: true}, "fixtures/no_ref_qual_types.json"},
{&Outer{}, &Reflector{ExpandedStruct: true, DoNotReference: true, YAMLEmbeddedStructs: true}, "fixtures/disable_inlining_embedded.json"},
+ {&MinValue{}, &Reflector{}, "fixtures/schema_with_minimum.json"},
{&TestNullable{}, &Reflector{}, "fixtures/nullable.json"},
+ {&TestYamlInline{}, &Reflector{YAMLEmbeddedStructs: true}, "fixtures/yaml_inline_embed.json"},
+ {&TestYamlInline{}, &Reflector{}, "fixtures/yaml_inline_embed.json"},
+ {&GrandfatherType{}, &Reflector{
+ AdditionalFields: func(r reflect.Type) []reflect.StructField {
+ return []reflect.StructField{
+ {
+ Name: "Addr",
+ Type: reflect.TypeOf((*net.IP)(nil)).Elem(),
+ Tag: "json:\"ip_addr\"",
+ Anonymous: false,
+ },
+ }
+ },
+ }, "fixtures/custom_additional.json"},
+ {&TestYamlAndJson{}, &Reflector{PreferYAMLSchema: true}, "fixtures/test_yaml_and_json_prefer_yaml.json"},
+ {&TestYamlAndJson{}, &Reflector{}, "fixtures/test_yaml_and_json.json"},
+ // {&TestYamlAndJson2{}, &Reflector{}, "fixtures/test_yaml_and_json2.json"},
+ {&CompactDate{}, &Reflector{}, "fixtures/compact_date.json"},
+ {&CustomSliceOuter{}, &Reflector{}, "fixtures/custom_slice_type.json"},
+ {&CustomMapOuter{}, &Reflector{}, "fixtures/custom_map_type.json"},
+ {&CustomTypeFieldWithInterface{}, &Reflector{}, "fixtures/custom_type_with_interface.json"},
+ {&examples.User{}, prepareCommentReflector(t), "fixtures/go_comments.json"},
}
for _, tt := range tests {
@@ -170,6 +312,14 @@ func TestSchemaGeneration(t *testing.T) {
}
}
+func prepareCommentReflector(t *testing.T) *Reflector {
+ t.Helper()
+ r := new(Reflector)
+ err := r.AddGoComments("github.com/alecthomas/jsonschema", "./examples")
+ require.NoError(t, err, "did not expect error while adding comments")
+ return r
+}
+
func TestBaselineUnmarshal(t *testing.T) {
expectedJSON, err := ioutil.ReadFile("fixtures/defaults.json")
require.NoError(t, err)
@@ -179,5 +329,5 @@ func TestBaselineUnmarshal(t *testing.T) {
actualJSON, _ := json.MarshalIndent(actualSchema, "", " ")
- require.Equal(t, strings.Replace(string(expectedJSON), `\/`, "/", -1), string(actualJSON))
+ require.Equal(t, strings.ReplaceAll(string(expectedJSON), `\/`, "/"), string(actualJSON))
}
Debdiff
[The following lists of changes regard files as different if they have different names, permissions or owners.]
Files in second set of .debs but not in first
-rw-r--r-- root/root /usr/share/doc/golang-github-alecthomas-jsonschema-dev/README.md -rw-r--r-- root/root /usr/share/gocode/src/github.com/alecthomas/jsonschema/comment_extractor.go -rw-r--r-- root/root /usr/share/gocode/src/github.com/alecthomas/jsonschema/examples/nested/nested.go -rw-r--r-- root/root /usr/share/gocode/src/github.com/alecthomas/jsonschema/examples/user.go -rw-r--r-- root/root /usr/share/gocode/src/github.com/alecthomas/jsonschema/fixtures/compact_date.json -rw-r--r-- root/root /usr/share/gocode/src/github.com/alecthomas/jsonschema/fixtures/custom_additional.json -rw-r--r-- root/root /usr/share/gocode/src/github.com/alecthomas/jsonschema/fixtures/custom_map_type.json -rw-r--r-- root/root /usr/share/gocode/src/github.com/alecthomas/jsonschema/fixtures/custom_slice_type.json -rw-r--r-- root/root /usr/share/gocode/src/github.com/alecthomas/jsonschema/fixtures/custom_type_with_interface.json -rw-r--r-- root/root /usr/share/gocode/src/github.com/alecthomas/jsonschema/fixtures/go_comments.json -rw-r--r-- root/root /usr/share/gocode/src/github.com/alecthomas/jsonschema/fixtures/schema_with_minimum.json -rw-r--r-- root/root /usr/share/gocode/src/github.com/alecthomas/jsonschema/fixtures/test_yaml_and_json.json -rw-r--r-- root/root /usr/share/gocode/src/github.com/alecthomas/jsonschema/fixtures/test_yaml_and_json2.json -rw-r--r-- root/root /usr/share/gocode/src/github.com/alecthomas/jsonschema/fixtures/test_yaml_and_json_prefer_yaml.json -rw-r--r-- root/root /usr/share/gocode/src/github.com/alecthomas/jsonschema/fixtures/yaml_inline.json -rw-r--r-- root/root /usr/share/gocode/src/github.com/alecthomas/jsonschema/fixtures/yaml_inline_embed.json
Files in first set of .debs but not in second
-rw-r--r-- root/root /usr/share/doc/golang-github-alecthomas-jsonschema-dev/README.md.gz
No differences were encountered in the control files