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:
|
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
|
||||||
|
|||||||
@@ -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 != "" {
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -35,8 +35,7 @@ func TestDefinition_MarshalJSON(t *testing.T) {
|
|||||||
"description":"A string type",
|
"description":"A string type",
|
||||||
"properties":{
|
"properties":{
|
||||||
"name":{
|
"name":{
|
||||||
"type":"string",
|
"type":"string"
|
||||||
"properties":{}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
@@ -66,12 +65,10 @@ func TestDefinition_MarshalJSON(t *testing.T) {
|
|||||||
"type":"object",
|
"type":"object",
|
||||||
"properties":{
|
"properties":{
|
||||||
"name":{
|
"name":{
|
||||||
"type":"string",
|
"type":"string"
|
||||||
"properties":{}
|
|
||||||
},
|
},
|
||||||
"age":{
|
"age":{
|
||||||
"type":"integer",
|
"type":"integer"
|
||||||
"properties":{}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,23 +111,19 @@ func TestDefinition_MarshalJSON(t *testing.T) {
|
|||||||
"type":"object",
|
"type":"object",
|
||||||
"properties":{
|
"properties":{
|
||||||
"name":{
|
"name":{
|
||||||
"type":"string",
|
"type":"string"
|
||||||
"properties":{}
|
|
||||||
},
|
},
|
||||||
"age":{
|
"age":{
|
||||||
"type":"integer",
|
"type":"integer"
|
||||||
"properties":{}
|
|
||||||
},
|
},
|
||||||
"address":{
|
"address":{
|
||||||
"type":"object",
|
"type":"object",
|
||||||
"properties":{
|
"properties":{
|
||||||
"city":{
|
"city":{
|
||||||
"type":"string",
|
"type":"string"
|
||||||
"properties":{}
|
|
||||||
},
|
},
|
||||||
"country":{
|
"country":{
|
||||||
"type":"string",
|
"type":"string"
|
||||||
"properties":{}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,15 +148,11 @@ func TestDefinition_MarshalJSON(t *testing.T) {
|
|||||||
want: `{
|
want: `{
|
||||||
"type":"array",
|
"type":"array",
|
||||||
"items":{
|
"items":{
|
||||||
"type":"string",
|
"type":"string"
|
||||||
"properties":{
|
|
||||||
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"properties":{
|
"properties":{
|
||||||
"name":{
|
"name":{
|
||||||
"type":"string",
|
"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)
|
||||||
|
|||||||
Reference in New Issue
Block a user