Skip to content

Commit e99eb54

Browse files
authored
add enum tag to jsonschema (#962)
* fix jsonschema tests * ensure all run during PR Github Action * add test for struct to schema * add support for enum tag * support nullable tag
1 parent 74d6449 commit e99eb54

File tree

3 files changed

+252
-72
lines changed

3 files changed

+252
-72
lines changed

.github/workflows/pr.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,6 @@ jobs:
2222
with:
2323
version: v1.64.5
2424
- name: Run tests
25-
run: go test -race -covermode=atomic -coverprofile=coverage.out -v .
25+
run: go test -race -covermode=atomic -coverprofile=coverage.out -v ./...
2626
- name: Upload coverage reports to Codecov
2727
uses: codecov/codecov-action@v4

jsonschema/json.go

+12
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ type Definition struct {
4646
// additionalProperties: false
4747
// additionalProperties: jsonschema.Definition{Type: jsonschema.String}
4848
AdditionalProperties any `json:"additionalProperties,omitempty"`
49+
// Whether the schema is nullable or not.
50+
Nullable bool `json:"nullable,omitempty"`
4951
}
5052

5153
func (d *Definition) MarshalJSON() ([]byte, error) {
@@ -139,6 +141,16 @@ func reflectSchemaObject(t reflect.Type) (*Definition, error) {
139141
if description != "" {
140142
item.Description = description
141143
}
144+
enum := field.Tag.Get("enum")
145+
if enum != "" {
146+
item.Enum = strings.Split(enum, ",")
147+
}
148+
149+
if n := field.Tag.Get("nullable"); n != "" {
150+
nullable, _ := strconv.ParseBool(n)
151+
item.Nullable = nullable
152+
}
153+
142154
properties[jsonTag] = *item
143155

144156
if s := field.Tag.Get("required"); s != "" {

jsonschema/json_test.go

+239-71
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ func TestDefinition_MarshalJSON(t *testing.T) {
1717
{
1818
name: "Test with empty Definition",
1919
def: jsonschema.Definition{},
20-
want: `{"properties":{}}`,
20+
want: `{}`,
2121
},
2222
{
2323
name: "Test with Definition properties set",
@@ -31,15 +31,14 @@ func TestDefinition_MarshalJSON(t *testing.T) {
3131
},
3232
},
3333
want: `{
34-
"type":"string",
35-
"description":"A string type",
36-
"properties":{
37-
"name":{
38-
"type":"string",
39-
"properties":{}
40-
}
41-
}
42-
}`,
34+
"type":"string",
35+
"description":"A string type",
36+
"properties":{
37+
"name":{
38+
"type":"string"
39+
}
40+
}
41+
}`,
4342
},
4443
{
4544
name: "Test with nested Definition properties",
@@ -60,23 +59,21 @@ func TestDefinition_MarshalJSON(t *testing.T) {
6059
},
6160
},
6261
want: `{
63-
"type":"object",
64-
"properties":{
65-
"user":{
66-
"type":"object",
67-
"properties":{
68-
"name":{
69-
"type":"string",
70-
"properties":{}
71-
},
72-
"age":{
73-
"type":"integer",
74-
"properties":{}
75-
}
76-
}
77-
}
78-
}
79-
}`,
62+
"type":"object",
63+
"properties":{
64+
"user":{
65+
"type":"object",
66+
"properties":{
67+
"name":{
68+
"type":"string"
69+
},
70+
"age":{
71+
"type":"integer"
72+
}
73+
}
74+
}
75+
}
76+
}`,
8077
},
8178
{
8279
name: "Test with complex nested Definition",
@@ -108,36 +105,32 @@ func TestDefinition_MarshalJSON(t *testing.T) {
108105
},
109106
},
110107
want: `{
111-
"type":"object",
112-
"properties":{
113-
"user":{
114-
"type":"object",
115-
"properties":{
116-
"name":{
117-
"type":"string",
118-
"properties":{}
119-
},
120-
"age":{
121-
"type":"integer",
122-
"properties":{}
123-
},
124-
"address":{
125-
"type":"object",
126-
"properties":{
127-
"city":{
128-
"type":"string",
129-
"properties":{}
130-
},
131-
"country":{
132-
"type":"string",
133-
"properties":{}
134-
}
135-
}
136-
}
137-
}
138-
}
139-
}
140-
}`,
108+
"type":"object",
109+
"properties":{
110+
"user":{
111+
"type":"object",
112+
"properties":{
113+
"name":{
114+
"type":"string"
115+
},
116+
"age":{
117+
"type":"integer"
118+
},
119+
"address":{
120+
"type":"object",
121+
"properties":{
122+
"city":{
123+
"type":"string"
124+
},
125+
"country":{
126+
"type":"string"
127+
}
128+
}
129+
}
130+
}
131+
}
132+
}
133+
}`,
141134
},
142135
{
143136
name: "Test with Array type Definition",
@@ -153,20 +146,16 @@ func TestDefinition_MarshalJSON(t *testing.T) {
153146
},
154147
},
155148
want: `{
156-
"type":"array",
157-
"items":{
158-
"type":"string",
159-
"properties":{
160-
161-
}
162-
},
163-
"properties":{
164-
"name":{
165-
"type":"string",
166-
"properties":{}
167-
}
168-
}
169-
}`,
149+
"type":"array",
150+
"items":{
151+
"type":"string"
152+
},
153+
"properties":{
154+
"name":{
155+
"type":"string"
156+
}
157+
}
158+
}`,
170159
},
171160
}
172161

@@ -193,6 +182,185 @@ func TestDefinition_MarshalJSON(t *testing.T) {
193182
}
194183
}
195184

185+
func TestStructToSchema(t *testing.T) {
186+
tests := []struct {
187+
name string
188+
in any
189+
want string
190+
}{
191+
{
192+
name: "Test with empty struct",
193+
in: struct{}{},
194+
want: `{
195+
"type":"object",
196+
"additionalProperties":false
197+
}`,
198+
},
199+
{
200+
name: "Test with struct containing many fields",
201+
in: struct {
202+
Name string `json:"name"`
203+
Age int `json:"age"`
204+
Active bool `json:"active"`
205+
Height float64 `json:"height"`
206+
Cities []struct {
207+
Name string `json:"name"`
208+
State string `json:"state"`
209+
} `json:"cities"`
210+
}{
211+
Name: "John Doe",
212+
Age: 30,
213+
Cities: []struct {
214+
Name string `json:"name"`
215+
State string `json:"state"`
216+
}{
217+
{Name: "New York", State: "NY"},
218+
{Name: "Los Angeles", State: "CA"},
219+
},
220+
},
221+
want: `{
222+
"type":"object",
223+
"properties":{
224+
"name":{
225+
"type":"string"
226+
},
227+
"age":{
228+
"type":"integer"
229+
},
230+
"active":{
231+
"type":"boolean"
232+
},
233+
"height":{
234+
"type":"number"
235+
},
236+
"cities":{
237+
"type":"array",
238+
"items":{
239+
"additionalProperties":false,
240+
"type":"object",
241+
"properties":{
242+
"name":{
243+
"type":"string"
244+
},
245+
"state":{
246+
"type":"string"
247+
}
248+
},
249+
"required":["name","state"]
250+
}
251+
}
252+
},
253+
"required":["name","age","active","height","cities"],
254+
"additionalProperties":false
255+
}`,
256+
},
257+
{
258+
name: "Test with description tag",
259+
in: struct {
260+
Name string `json:"name" description:"The name of the person"`
261+
}{
262+
Name: "John Doe",
263+
},
264+
want: `{
265+
"type":"object",
266+
"properties":{
267+
"name":{
268+
"type":"string",
269+
"description":"The name of the person"
270+
}
271+
},
272+
"required":["name"],
273+
"additionalProperties":false
274+
}`,
275+
},
276+
{
277+
name: "Test with required tag",
278+
in: struct {
279+
Name string `json:"name" required:"false"`
280+
}{
281+
Name: "John Doe",
282+
},
283+
want: `{
284+
"type":"object",
285+
"properties":{
286+
"name":{
287+
"type":"string"
288+
}
289+
},
290+
"additionalProperties":false
291+
}`,
292+
},
293+
{
294+
name: "Test with enum tag",
295+
in: struct {
296+
Color string `json:"color" enum:"red,green,blue"`
297+
}{
298+
Color: "red",
299+
},
300+
want: `{
301+
"type":"object",
302+
"properties":{
303+
"color":{
304+
"type":"string",
305+
"enum":["red","green","blue"]
306+
}
307+
},
308+
"required":["color"],
309+
"additionalProperties":false
310+
}`,
311+
},
312+
{
313+
name: "Test with nullable tag",
314+
in: struct {
315+
Name *string `json:"name" nullable:"true"`
316+
}{
317+
Name: nil,
318+
},
319+
want: `{
320+
321+
"type":"object",
322+
"properties":{
323+
"name":{
324+
"type":"string",
325+
"nullable":true
326+
}
327+
},
328+
"required":["name"],
329+
"additionalProperties":false
330+
}`,
331+
},
332+
}
333+
334+
for _, tt := range tests {
335+
t.Run(tt.name, func(t *testing.T) {
336+
wantBytes := []byte(tt.want)
337+
338+
schema, err := jsonschema.GenerateSchemaForType(tt.in)
339+
if err != nil {
340+
t.Errorf("Failed to generate schema: error = %v", err)
341+
return
342+
}
343+
344+
var want map[string]interface{}
345+
err = json.Unmarshal(wantBytes, &want)
346+
if err != nil {
347+
t.Errorf("Failed to Unmarshal JSON: error = %v", err)
348+
return
349+
}
350+
351+
got := structToMap(t, schema)
352+
gotPtr := structToMap(t, &schema)
353+
354+
if !reflect.DeepEqual(got, want) {
355+
t.Errorf("MarshalJSON() got = %v, want %v", got, want)
356+
}
357+
if !reflect.DeepEqual(gotPtr, want) {
358+
t.Errorf("MarshalJSON() gotPtr = %v, want %v", gotPtr, want)
359+
}
360+
})
361+
}
362+
}
363+
196364
func structToMap(t *testing.T, v any) map[string]any {
197365
t.Helper()
198366
gotBytes, err := json.Marshal(v)

0 commit comments

Comments
 (0)