Skip to content

add enum tag to jsonschema #962

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ jobs:
with:
version: v1.64.5
- 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
uses: codecov/codecov-action@v4
12 changes: 12 additions & 0 deletions jsonschema/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ type Definition struct {
// additionalProperties: false
// additionalProperties: jsonschema.Definition{Type: jsonschema.String}
AdditionalProperties any `json:"additionalProperties,omitempty"`
// Whether the schema is nullable or not.
Nullable bool `json:"nullable,omitempty"`
}

func (d *Definition) MarshalJSON() ([]byte, error) {
Expand Down Expand Up @@ -139,6 +141,16 @@ func reflectSchemaObject(t reflect.Type) (*Definition, error) {
if 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

if s := field.Tag.Get("required"); s != "" {
Expand Down
310 changes: 239 additions & 71 deletions jsonschema/json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func TestDefinition_MarshalJSON(t *testing.T) {
{
name: "Test with empty Definition",
def: jsonschema.Definition{},
want: `{"properties":{}}`,
want: `{}`,
},
{
name: "Test with Definition properties set",
Expand All @@ -31,15 +31,14 @@ func TestDefinition_MarshalJSON(t *testing.T) {
},
},
want: `{
"type":"string",
"description":"A string type",
"properties":{
"name":{
"type":"string",
"properties":{}
}
}
}`,
"type":"string",
"description":"A string type",
"properties":{
"name":{
"type":"string"
}
}
}`,
},
{
name: "Test with nested Definition properties",
Expand All @@ -60,23 +59,21 @@ func TestDefinition_MarshalJSON(t *testing.T) {
},
},
want: `{
"type":"object",
"properties":{
"user":{
"type":"object",
"properties":{
"name":{
"type":"string",
"properties":{}
},
"age":{
"type":"integer",
"properties":{}
}
}
}
}
}`,
"type":"object",
"properties":{
"user":{
"type":"object",
"properties":{
"name":{
"type":"string"
},
"age":{
"type":"integer"
}
}
}
}
}`,
},
{
name: "Test with complex nested Definition",
Expand Down Expand Up @@ -108,36 +105,32 @@ func TestDefinition_MarshalJSON(t *testing.T) {
},
},
want: `{
"type":"object",
"properties":{
"user":{
"type":"object",
"properties":{
"name":{
"type":"string",
"properties":{}
},
"age":{
"type":"integer",
"properties":{}
},
"address":{
"type":"object",
"properties":{
"city":{
"type":"string",
"properties":{}
},
"country":{
"type":"string",
"properties":{}
}
}
}
}
}
}
}`,
"type":"object",
"properties":{
"user":{
"type":"object",
"properties":{
"name":{
"type":"string"
},
"age":{
"type":"integer"
},
"address":{
"type":"object",
"properties":{
"city":{
"type":"string"
},
"country":{
"type":"string"
}
}
}
}
}
}
}`,
},
{
name: "Test with Array type Definition",
Expand All @@ -153,20 +146,16 @@ func TestDefinition_MarshalJSON(t *testing.T) {
},
},
want: `{
"type":"array",
"items":{
"type":"string",
"properties":{

}
},
"properties":{
"name":{
"type":"string",
"properties":{}
}
}
}`,
"type":"array",
"items":{
"type":"string"
},
"properties":{
"name":{
"type":"string"
}
}
}`,
},
}

Expand All @@ -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 {
t.Helper()
gotBytes, err := json.Marshal(v)
Expand Down
Loading