From 06ae07cd2fe1b81754b5161d65f8988672d05c2d Mon Sep 17 00:00:00 2001 From: JT Archie Date: Wed, 2 Apr 2025 18:20:02 -0600 Subject: [PATCH 1/5] fix jsonschema tests --- jsonschema/json_test.go | 31 ++++++++++--------------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/jsonschema/json_test.go b/jsonschema/json_test.go index 744706082..83d6b10e1 100644 --- a/jsonschema/json_test.go +++ b/jsonschema/json_test.go @@ -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", @@ -35,8 +35,7 @@ func TestDefinition_MarshalJSON(t *testing.T) { "description":"A string type", "properties":{ "name":{ - "type":"string", - "properties":{} + "type":"string" } } }`, @@ -66,12 +65,10 @@ func TestDefinition_MarshalJSON(t *testing.T) { "type":"object", "properties":{ "name":{ - "type":"string", - "properties":{} + "type":"string" }, "age":{ - "type":"integer", - "properties":{} + "type":"integer" } } } @@ -114,23 +111,19 @@ func TestDefinition_MarshalJSON(t *testing.T) { "type":"object", "properties":{ "name":{ - "type":"string", - "properties":{} + "type":"string" }, "age":{ - "type":"integer", - "properties":{} + "type":"integer" }, "address":{ "type":"object", "properties":{ "city":{ - "type":"string", - "properties":{} + "type":"string" }, "country":{ - "type":"string", - "properties":{} + "type":"string" } } } @@ -155,15 +148,11 @@ func TestDefinition_MarshalJSON(t *testing.T) { want: `{ "type":"array", "items":{ - "type":"string", - "properties":{ - - } + "type":"string" }, "properties":{ "name":{ - "type":"string", - "properties":{} + "type":"string" } } }`, From ad07c3664d81fc04db6800ad688e3b5f351ab04d Mon Sep 17 00:00:00 2001 From: JT Archie Date: Wed, 2 Apr 2025 18:21:04 -0600 Subject: [PATCH 2/5] ensure all run during PR Github Action --- .github/workflows/pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 818a8842b..f4cbe7c8b 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -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 From 650dacf5ea64ac2e732299fef18ffb0b4affe1f0 Mon Sep 17 00:00:00 2001 From: JT Archie Date: Wed, 2 Apr 2025 18:31:16 -0600 Subject: [PATCH 3/5] add test for struct to schema --- jsonschema/json_test.go | 258 +++++++++++++++++++++++++++++++--------- 1 file changed, 199 insertions(+), 59 deletions(-) diff --git a/jsonschema/json_test.go b/jsonschema/json_test.go index 83d6b10e1..d3e351f54 100644 --- a/jsonschema/json_test.go +++ b/jsonschema/json_test.go @@ -31,14 +31,14 @@ func TestDefinition_MarshalJSON(t *testing.T) { }, }, want: `{ - "type":"string", - "description":"A string type", - "properties":{ - "name":{ - "type":"string" - } - } -}`, + "type":"string", + "description":"A string type", + "properties":{ + "name":{ + "type":"string" + } + } + }`, }, { name: "Test with nested Definition properties", @@ -59,21 +59,21 @@ func TestDefinition_MarshalJSON(t *testing.T) { }, }, want: `{ - "type":"object", - "properties":{ - "user":{ - "type":"object", - "properties":{ - "name":{ - "type":"string" - }, - "age":{ - "type":"integer" - } - } - } - } -}`, + "type":"object", + "properties":{ + "user":{ + "type":"object", + "properties":{ + "name":{ + "type":"string" + }, + "age":{ + "type":"integer" + } + } + } + } + }`, }, { name: "Test with complex nested Definition", @@ -105,32 +105,32 @@ func TestDefinition_MarshalJSON(t *testing.T) { }, }, want: `{ - "type":"object", - "properties":{ - "user":{ - "type":"object", - "properties":{ - "name":{ - "type":"string" - }, - "age":{ - "type":"integer" - }, - "address":{ - "type":"object", - "properties":{ - "city":{ - "type":"string" - }, - "country":{ - "type":"string" - } - } - } - } - } - } -}`, + "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", @@ -146,16 +146,16 @@ func TestDefinition_MarshalJSON(t *testing.T) { }, }, want: `{ - "type":"array", - "items":{ - "type":"string" - }, - "properties":{ - "name":{ - "type":"string" - } - } -}`, + "type":"array", + "items":{ + "type":"string" + }, + "properties":{ + "name":{ + "type":"string" + } + } + }`, }, } @@ -182,6 +182,146 @@ 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 + }`, + }, + } + + 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) From db50e2bcce34fad9caf3b025cc64f1f98b55ddee Mon Sep 17 00:00:00 2001 From: JT Archie Date: Wed, 2 Apr 2025 18:45:57 -0600 Subject: [PATCH 4/5] add support for enum tag --- jsonschema/json.go | 5 +++++ jsonschema/json_test.go | 27 +++++++++++++++++++++++---- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/jsonschema/json.go b/jsonschema/json.go index bcb253fae..615780bb4 100644 --- a/jsonschema/json.go +++ b/jsonschema/json.go @@ -139,6 +139,11 @@ func reflectSchemaObject(t reflect.Type) (*Definition, error) { if description != "" { item.Description = description } + enum := field.Tag.Get("enum") + if enum != "" { + item.Enum = strings.Split(enum, ",") + } + properties[jsonTag] = *item if s := field.Tag.Get("required"); s != "" { diff --git a/jsonschema/json_test.go b/jsonschema/json_test.go index d3e351f54..cc8fd9cb5 100644 --- a/jsonschema/json_test.go +++ b/jsonschema/json_test.go @@ -199,9 +199,9 @@ func TestStructToSchema(t *testing.T) { { name: "Test with struct containing many fields", in: struct { - Name string `json:"name"` - Age int `json:"age"` - Active bool `json:"active"` + Name string `json:"name"` + Age int `json:"age"` + Active bool `json:"active"` Height float64 `json:"height"` Cities []struct { Name string `json:"name"` @@ -257,7 +257,7 @@ func TestStructToSchema(t *testing.T) { { name: "Test with description tag", in: struct { - Name string `json:"name" description:"The name of the person"` + Name string `json:"name" description:"The name of the person"` }{ Name: "John Doe", }, @@ -290,6 +290,25 @@ func TestStructToSchema(t *testing.T) { "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 + }`, + }, } for _, tt := range tests { From 685bbf9badd2541e3a15bbde3db862e85b9483de Mon Sep 17 00:00:00 2001 From: JT Archie Date: Tue, 8 Apr 2025 13:13:35 -0600 Subject: [PATCH 5/5] support nullable tag --- jsonschema/json.go | 7 +++++++ jsonschema/json_test.go | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/jsonschema/json.go b/jsonschema/json.go index 615780bb4..d458418f3 100644 --- a/jsonschema/json.go +++ b/jsonschema/json.go @@ -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) { @@ -144,6 +146,11 @@ func reflectSchemaObject(t reflect.Type) (*Definition, error) { 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 != "" { diff --git a/jsonschema/json_test.go b/jsonschema/json_test.go index cc8fd9cb5..17f0aba8a 100644 --- a/jsonschema/json_test.go +++ b/jsonschema/json_test.go @@ -309,6 +309,26 @@ func TestStructToSchema(t *testing.T) { "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 {