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:
2
.github/workflows/pr.yml
vendored
2
.github/workflows/pr.yml
vendored
@@ -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
|
||||
|
||||
@@ -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 != "" {
|
||||
|
||||
@@ -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,11 +35,10 @@ func TestDefinition_MarshalJSON(t *testing.T) {
|
||||
"description":"A string type",
|
||||
"properties":{
|
||||
"name":{
|
||||
"type":"string",
|
||||
"properties":{}
|
||||
"type":"string"
|
||||
}
|
||||
}
|
||||
}`,
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "Test with nested Definition properties",
|
||||
@@ -66,17 +65,15 @@ func TestDefinition_MarshalJSON(t *testing.T) {
|
||||
"type":"object",
|
||||
"properties":{
|
||||
"name":{
|
||||
"type":"string",
|
||||
"properties":{}
|
||||
"type":"string"
|
||||
},
|
||||
"age":{
|
||||
"type":"integer",
|
||||
"properties":{}
|
||||
"type":"integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "Test with complex nested Definition",
|
||||
@@ -114,30 +111,26 @@ 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "Test with Array type Definition",
|
||||
@@ -155,18 +148,14 @@ func TestDefinition_MarshalJSON(t *testing.T) {
|
||||
want: `{
|
||||
"type":"array",
|
||||
"items":{
|
||||
"type":"string",
|
||||
"properties":{
|
||||
|
||||
}
|
||||
"type":"string"
|
||||
},
|
||||
"properties":{
|
||||
"name":{
|
||||
"type":"string",
|
||||
"properties":{}
|
||||
"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)
|
||||
|
||||
Reference in New Issue
Block a user