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
This commit is contained in:
JT A.
2025-04-13 12:00:48 -06:00
committed by GitHub
parent 74d6449f22
commit e99eb54c9d
3 changed files with 252 additions and 72 deletions

View File

@@ -22,6 +22,6 @@ jobs:
with: with:
version: v1.64.5 version: v1.64.5
- name: Run tests - name: Run tests
run: go test -race -covermode=atomic -coverprofile=coverage.out -v . run: go test -race -covermode=atomic -coverprofile=coverage.out -v ./...
- name: Upload coverage reports to Codecov - name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v4 uses: codecov/codecov-action@v4

View File

@@ -46,6 +46,8 @@ type Definition struct {
// additionalProperties: false // additionalProperties: false
// additionalProperties: jsonschema.Definition{Type: jsonschema.String} // additionalProperties: jsonschema.Definition{Type: jsonschema.String}
AdditionalProperties any `json:"additionalProperties,omitempty"` AdditionalProperties any `json:"additionalProperties,omitempty"`
// Whether the schema is nullable or not.
Nullable bool `json:"nullable,omitempty"`
} }
func (d *Definition) MarshalJSON() ([]byte, error) { func (d *Definition) MarshalJSON() ([]byte, error) {
@@ -139,6 +141,16 @@ func reflectSchemaObject(t reflect.Type) (*Definition, error) {
if description != "" { if description != "" {
item.Description = description item.Description = description
} }
enum := field.Tag.Get("enum")
if enum != "" {
item.Enum = strings.Split(enum, ",")
}
if n := field.Tag.Get("nullable"); n != "" {
nullable, _ := strconv.ParseBool(n)
item.Nullable = nullable
}
properties[jsonTag] = *item properties[jsonTag] = *item
if s := field.Tag.Get("required"); s != "" { if s := field.Tag.Get("required"); s != "" {

View File

@@ -17,7 +17,7 @@ func TestDefinition_MarshalJSON(t *testing.T) {
{ {
name: "Test with empty Definition", name: "Test with empty Definition",
def: jsonschema.Definition{}, def: jsonschema.Definition{},
want: `{"properties":{}}`, want: `{}`,
}, },
{ {
name: "Test with Definition properties set", name: "Test with Definition properties set",
@@ -31,15 +31,14 @@ func TestDefinition_MarshalJSON(t *testing.T) {
}, },
}, },
want: `{ want: `{
"type":"string", "type":"string",
"description":"A string type", "description":"A string type",
"properties":{ "properties":{
"name":{ "name":{
"type":"string", "type":"string"
"properties":{} }
} }
} }`,
}`,
}, },
{ {
name: "Test with nested Definition properties", name: "Test with nested Definition properties",
@@ -60,23 +59,21 @@ func TestDefinition_MarshalJSON(t *testing.T) {
}, },
}, },
want: `{ want: `{
"type":"object", "type":"object",
"properties":{ "properties":{
"user":{ "user":{
"type":"object", "type":"object",
"properties":{ "properties":{
"name":{ "name":{
"type":"string", "type":"string"
"properties":{} },
}, "age":{
"age":{ "type":"integer"
"type":"integer", }
"properties":{} }
} }
} }
} }`,
}
}`,
}, },
{ {
name: "Test with complex nested Definition", name: "Test with complex nested Definition",
@@ -108,36 +105,32 @@ func TestDefinition_MarshalJSON(t *testing.T) {
}, },
}, },
want: `{ want: `{
"type":"object", "type":"object",
"properties":{ "properties":{
"user":{ "user":{
"type":"object", "type":"object",
"properties":{ "properties":{
"name":{ "name":{
"type":"string", "type":"string"
"properties":{} },
}, "age":{
"age":{ "type":"integer"
"type":"integer", },
"properties":{} "address":{
}, "type":"object",
"address":{ "properties":{
"type":"object", "city":{
"properties":{ "type":"string"
"city":{ },
"type":"string", "country":{
"properties":{} "type":"string"
}, }
"country":{ }
"type":"string", }
"properties":{} }
} }
} }
} }`,
}
}
}
}`,
}, },
{ {
name: "Test with Array type Definition", name: "Test with Array type Definition",
@@ -153,20 +146,16 @@ func TestDefinition_MarshalJSON(t *testing.T) {
}, },
}, },
want: `{ want: `{
"type":"array", "type":"array",
"items":{ "items":{
"type":"string", "type":"string"
"properties":{ },
"properties":{
} "name":{
}, "type":"string"
"properties":{ }
"name":{ }
"type":"string", }`,
"properties":{}
}
}
}`,
}, },
} }
@@ -193,6 +182,185 @@ func TestDefinition_MarshalJSON(t *testing.T) {
} }
} }
func TestStructToSchema(t *testing.T) {
tests := []struct {
name string
in any
want string
}{
{
name: "Test with empty struct",
in: struct{}{},
want: `{
"type":"object",
"additionalProperties":false
}`,
},
{
name: "Test with struct containing many fields",
in: struct {
Name string `json:"name"`
Age int `json:"age"`
Active bool `json:"active"`
Height float64 `json:"height"`
Cities []struct {
Name string `json:"name"`
State string `json:"state"`
} `json:"cities"`
}{
Name: "John Doe",
Age: 30,
Cities: []struct {
Name string `json:"name"`
State string `json:"state"`
}{
{Name: "New York", State: "NY"},
{Name: "Los Angeles", State: "CA"},
},
},
want: `{
"type":"object",
"properties":{
"name":{
"type":"string"
},
"age":{
"type":"integer"
},
"active":{
"type":"boolean"
},
"height":{
"type":"number"
},
"cities":{
"type":"array",
"items":{
"additionalProperties":false,
"type":"object",
"properties":{
"name":{
"type":"string"
},
"state":{
"type":"string"
}
},
"required":["name","state"]
}
}
},
"required":["name","age","active","height","cities"],
"additionalProperties":false
}`,
},
{
name: "Test with description tag",
in: struct {
Name string `json:"name" description:"The name of the person"`
}{
Name: "John Doe",
},
want: `{
"type":"object",
"properties":{
"name":{
"type":"string",
"description":"The name of the person"
}
},
"required":["name"],
"additionalProperties":false
}`,
},
{
name: "Test with required tag",
in: struct {
Name string `json:"name" required:"false"`
}{
Name: "John Doe",
},
want: `{
"type":"object",
"properties":{
"name":{
"type":"string"
}
},
"additionalProperties":false
}`,
},
{
name: "Test with enum tag",
in: struct {
Color string `json:"color" enum:"red,green,blue"`
}{
Color: "red",
},
want: `{
"type":"object",
"properties":{
"color":{
"type":"string",
"enum":["red","green","blue"]
}
},
"required":["color"],
"additionalProperties":false
}`,
},
{
name: "Test with nullable tag",
in: struct {
Name *string `json:"name" nullable:"true"`
}{
Name: nil,
},
want: `{
"type":"object",
"properties":{
"name":{
"type":"string",
"nullable":true
}
},
"required":["name"],
"additionalProperties":false
}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
wantBytes := []byte(tt.want)
schema, err := jsonschema.GenerateSchemaForType(tt.in)
if err != nil {
t.Errorf("Failed to generate schema: error = %v", err)
return
}
var want map[string]interface{}
err = json.Unmarshal(wantBytes, &want)
if err != nil {
t.Errorf("Failed to Unmarshal JSON: error = %v", err)
return
}
got := structToMap(t, schema)
gotPtr := structToMap(t, &schema)
if !reflect.DeepEqual(got, want) {
t.Errorf("MarshalJSON() got = %v, want %v", got, want)
}
if !reflect.DeepEqual(gotPtr, want) {
t.Errorf("MarshalJSON() gotPtr = %v, want %v", gotPtr, want)
}
})
}
}
func structToMap(t *testing.T, v any) map[string]any { func structToMap(t *testing.T, v any) map[string]any {
t.Helper() t.Helper()
gotBytes, err := json.Marshal(v) gotBytes, err := json.Marshal(v)