1# Go JSON Schema Reflection
2
3[](https://github.com/invopop/jsonschema/actions/workflows/lint.yaml)
4[](https://github.com/invopop/jsonschema/actions/workflows/test.yaml)
5[](https://goreportcard.com/report/github.com/invopop/jsonschema)
6[](https://godoc.org/github.com/invopop/jsonschema)
7[](https://codecov.io/gh/invopop/jsonschema)
8
9
10This package can be used to generate [JSON Schemas](http://json-schema.org/latest/json-schema-validation.html) from Go types through reflection.
11
12- Supports arbitrarily complex types, including `interface{}`, maps, slices, etc.
13- Supports json-schema features such as minLength, maxLength, pattern, format, etc.
14- Supports simple string and numeric enums.
15- Supports custom property fields via the `jsonschema_extras` struct tag.
16
17This repository is a fork of the original [jsonschema](https://github.com/alecthomas/jsonschema) by [@alecthomas](https://github.com/alecthomas). At [Invopop](https://invopop.com) we use jsonschema as a cornerstone in our [GOBL library](https://github.com/invopop/gobl), and wanted to be able to continue building and adding features without taking up Alec's time. There have been a few significant changes that probably mean this version is a not compatible with with Alec's:
18
19- The original was stuck on the draft-04 version of JSON Schema, we've now moved to the latest JSON Schema Draft 2020-12.
20- Schema IDs are added automatically from the current Go package's URL in order to be unique, and can be disabled with the `Anonymous` option.
21- Support for the `FullyQualifyTypeName` option has been removed. If you have conflicts, you should use multiple schema files with different IDs, set the `DoNotReference` option to true to hide definitions completely, or add your own naming strategy using the `Namer` property.
22- Support for `yaml` tags and related options has been dropped for the sake of simplification. There were a [few inconsistencies](https://github.com/invopop/jsonschema/pull/21) around this that have now been fixed.
23
24## Versions
25
26This project is still under v0 scheme, as per Go convention, breaking changes are likely. Please pin go modules to version tags or branches, and reach out if you think something can be improved.
27
28Go version >= 1.18 is required as generics are now being used.
29
30## Example
31
32The following Go type:
33
34```go
35type TestUser struct {
36 ID int `json:"id"`
37 Name string `json:"name" jsonschema:"title=the name,description=The name of a friend,example=joe,example=lucy,default=alex"`
38 Friends []int `json:"friends,omitempty" jsonschema_description:"The list of IDs, omitted when empty"`
39 Tags map[string]interface{} `json:"tags,omitempty" jsonschema_extras:"a=b,foo=bar,foo=bar1"`
40 BirthDate time.Time `json:"birth_date,omitempty" jsonschema:"oneof_required=date"`
41 YearOfBirth string `json:"year_of_birth,omitempty" jsonschema:"oneof_required=year"`
42 Metadata interface{} `json:"metadata,omitempty" jsonschema:"oneof_type=string;array"`
43 FavColor string `json:"fav_color,omitempty" jsonschema:"enum=red,enum=green,enum=blue"`
44}
45```
46
47Results in following JSON Schema:
48
49```go
50jsonschema.Reflect(&TestUser{})
51```
52
53```json
54{
55 "$schema": "https://json-schema.org/draft/2020-12/schema",
56 "$id": "https://github.com/invopop/jsonschema_test/test-user",
57 "$ref": "#/$defs/TestUser",
58 "$defs": {
59 "TestUser": {
60 "oneOf": [
61 {
62 "required": ["birth_date"],
63 "title": "date"
64 },
65 {
66 "required": ["year_of_birth"],
67 "title": "year"
68 }
69 ],
70 "properties": {
71 "id": {
72 "type": "integer"
73 },
74 "name": {
75 "type": "string",
76 "title": "the name",
77 "description": "The name of a friend",
78 "default": "alex",
79 "examples": ["joe", "lucy"]
80 },
81 "friends": {
82 "items": {
83 "type": "integer"
84 },
85 "type": "array",
86 "description": "The list of IDs, omitted when empty"
87 },
88 "tags": {
89 "type": "object",
90 "a": "b",
91 "foo": ["bar", "bar1"]
92 },
93 "birth_date": {
94 "type": "string",
95 "format": "date-time"
96 },
97 "year_of_birth": {
98 "type": "string"
99 },
100 "metadata": {
101 "oneOf": [
102 {
103 "type": "string"
104 },
105 {
106 "type": "array"
107 }
108 ]
109 },
110 "fav_color": {
111 "type": "string",
112 "enum": ["red", "green", "blue"]
113 }
114 },
115 "additionalProperties": false,
116 "type": "object",
117 "required": ["id", "name"]
118 }
119 }
120}
121```
122
123## YAML
124
125Support for `yaml` tags has now been removed. If you feel very strongly about this, we've opened a discussion to hear your comments: https://github.com/invopop/jsonschema/discussions/28
126
127The recommended approach if you need to deal with YAML data is to first convert to JSON. The [invopop/yaml](https://github.com/invopop/yaml) library will make this trivial.
128
129## Configurable behaviour
130
131The behaviour of the schema generator can be altered with parameters when a `jsonschema.Reflector`
132instance is created.
133
134### ExpandedStruct
135
136If set to `true`, makes the top level struct not to reference itself in the definitions. But type passed should be a struct type.
137
138eg.
139
140```go
141type GrandfatherType struct {
142 FamilyName string `json:"family_name" jsonschema:"required"`
143}
144
145type SomeBaseType struct {
146 SomeBaseProperty int `json:"some_base_property"`
147 // The jsonschema required tag is nonsensical for private and ignored properties.
148 // Their presence here tests that the fields *will not* be required in the output
149 // schema, even if they are tagged required.
150 somePrivateBaseProperty string `json:"i_am_private" jsonschema:"required"`
151 SomeIgnoredBaseProperty string `json:"-" jsonschema:"required"`
152 SomeSchemaIgnoredProperty string `jsonschema:"-,required"`
153 SomeUntaggedBaseProperty bool `jsonschema:"required"`
154 someUnexportedUntaggedBaseProperty bool
155 Grandfather GrandfatherType `json:"grand"`
156}
157```
158
159will output:
160
161```json
162{
163 "$schema": "http://json-schema.org/draft/2020-12/schema",
164 "required": ["some_base_property", "grand", "SomeUntaggedBaseProperty"],
165 "properties": {
166 "SomeUntaggedBaseProperty": {
167 "type": "boolean"
168 },
169 "grand": {
170 "$schema": "http://json-schema.org/draft/2020-12/schema",
171 "$ref": "#/definitions/GrandfatherType"
172 },
173 "some_base_property": {
174 "type": "integer"
175 }
176 },
177 "type": "object",
178 "$defs": {
179 "GrandfatherType": {
180 "required": ["family_name"],
181 "properties": {
182 "family_name": {
183 "type": "string"
184 }
185 },
186 "additionalProperties": false,
187 "type": "object"
188 }
189 }
190}
191```
192
193### Using Go Comments
194
195Writing a good schema with descriptions inside tags can become cumbersome and tedious, especially if you already have some Go comments around your types and field definitions. If you'd like to take advantage of these existing comments, you can use the `AddGoComments(base, path string)` method that forms part of the reflector to parse your go files and automatically generate a dictionary of Go import paths, types, and fields, to individual comments. These will then be used automatically as description fields, and can be overridden with a manual definition if needed.
196
197Take a simplified example of a User struct which for the sake of simplicity we assume is defined inside this package:
198
199```go
200package main
201
202// User is used as a base to provide tests for comments.
203type User struct {
204 // Unique sequential identifier.
205 ID int `json:"id" jsonschema:"required"`
206 // Name of the user
207 Name string `json:"name"`
208}
209```
210
211To get the comments provided into your JSON schema, use a regular `Reflector` and add the go code using an import module URL and path. Fully qualified go module paths cannot be determined reliably by the `go/parser` library, so we need to introduce this manually:
212
213```go
214r := new(Reflector)
215if err := r.AddGoComments("github.com/invopop/jsonschema", "./"); err != nil {
216 // deal with error
217}
218s := r.Reflect(&User{})
219// output
220```
221
222Expect the results to be similar to:
223
224```json
225{
226 "$schema": "http://json-schema.org/draft/2020-12/schema",
227 "$ref": "#/$defs/User",
228 "$defs": {
229 "User": {
230 "required": ["id"],
231 "properties": {
232 "id": {
233 "type": "integer",
234 "description": "Unique sequential identifier."
235 },
236 "name": {
237 "type": "string",
238 "description": "Name of the user"
239 }
240 },
241 "additionalProperties": false,
242 "type": "object",
243 "description": "User is used as a base to provide tests for comments."
244 }
245 }
246}
247```
248
249### Custom Key Naming
250
251In some situations, the keys actually used to write files are different from Go structs'.
252
253This is often the case when writing a configuration file to YAML or JSON from a Go struct, or when returning a JSON response for a Web API: APIs typically use snake_case, while Go uses PascalCase.
254
255You can pass a `func(string) string` function to `Reflector`'s `KeyNamer` option to map Go field names to JSON key names and reflect the aforementioned transformations, without having to specify `json:"..."` on every struct field.
256
257For example, consider the following struct
258
259```go
260type User struct {
261 GivenName string
262 PasswordSalted []byte `json:"salted_password"`
263}
264```
265
266We can transform field names to snake_case in the generated JSON schema:
267
268```go
269r := new(jsonschema.Reflector)
270r.KeyNamer = strcase.SnakeCase // from package github.com/stoewer/go-strcase
271
272r.Reflect(&User{})
273```
274
275Will yield
276
277```diff
278 {
279 "$schema": "http://json-schema.org/draft/2020-12/schema",
280 "$ref": "#/$defs/User",
281 "$defs": {
282 "User": {
283 "properties": {
284- "GivenName": {
285+ "given_name": {
286 "type": "string"
287 },
288 "salted_password": {
289 "type": "string",
290 "contentEncoding": "base64"
291 }
292 },
293 "additionalProperties": false,
294 "type": "object",
295- "required": ["GivenName", "salted_password"]
296+ "required": ["given_name", "salted_password"]
297 }
298 }
299 }
300```
301
302As you can see, if a field name has a `json:""` tag set, the `key` argument to `KeyNamer` will have the value of that tag.
303
304### Custom Type Definitions
305
306Sometimes it can be useful to have custom JSON Marshal and Unmarshal methods in your structs that automatically convert for example a string into an object.
307
308This library will recognize and attempt to call four different methods that help you adjust schemas to your specific needs:
309
310- `JSONSchema() *Schema` - will prevent auto-generation of the schema so that you can provide your own definition.
311- `JSONSchemaExtend(schema *jsonschema.Schema)` - will be called _after_ the schema has been generated, allowing you to add or manipulate the fields easily.
312- `JSONSchemaAlias() any` - is called when reflecting the type of object and allows for an alternative to be used instead.
313- `JSONSchemaProperty(prop string) any` - will be called for every property inside a struct giving you the chance to provide an alternative object to convert into a schema.
314
315Note that all of these methods **must** be defined on a non-pointer object for them to be called.
316
317Take the following simplified example of a `CompactDate` that only includes the Year and Month:
318
319```go
320type CompactDate struct {
321 Year int
322 Month int
323}
324
325func (d *CompactDate) UnmarshalJSON(data []byte) error {
326 if len(data) != 9 {
327 return errors.New("invalid compact date length")
328 }
329 var err error
330 d.Year, err = strconv.Atoi(string(data[1:5]))
331 if err != nil {
332 return err
333 }
334 d.Month, err = strconv.Atoi(string(data[7:8]))
335 if err != nil {
336 return err
337 }
338 return nil
339}
340
341func (d *CompactDate) MarshalJSON() ([]byte, error) {
342 buf := new(bytes.Buffer)
343 buf.WriteByte('"')
344 buf.WriteString(fmt.Sprintf("%d-%02d", d.Year, d.Month))
345 buf.WriteByte('"')
346 return buf.Bytes(), nil
347}
348
349func (CompactDate) JSONSchema() *Schema {
350 return &Schema{
351 Type: "string",
352 Title: "Compact Date",
353 Description: "Short date that only includes year and month",
354 Pattern: "^[0-9]{4}-[0-1][0-9]$",
355 }
356}
357```
358
359The resulting schema generated for this struct would look like:
360
361```json
362{
363 "$schema": "http://json-schema.org/draft/2020-12/schema",
364 "$ref": "#/$defs/CompactDate",
365 "$defs": {
366 "CompactDate": {
367 "pattern": "^[0-9]{4}-[0-1][0-9]$",
368 "type": "string",
369 "title": "Compact Date",
370 "description": "Short date that only includes year and month"
371 }
372 }
373}
374```