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 diff --git a/jsonschema/json.go b/jsonschema/json.go index bcb253fae..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) { @@ -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 != "" { diff --git a/jsonschema/json_test.go b/jsonschema/json_test.go index 744706082..17f0aba8a 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", @@ -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", @@ -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", @@ -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", @@ -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" + } + } + }`, }, } @@ -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)