Skip to content
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
18 changes: 18 additions & 0 deletions transformers/json_schema_fields.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,25 @@ type embeddedJSONStruct struct {
addressable bool
}

type jsonSchemaFieldsKey struct {
structType reflect.Type
addressable bool
}

func (t *structTransformer) jsonSchemaFields(structType reflect.Type, addressable bool) map[string]jsonSchemaField {
key := jsonSchemaFieldsKey{structType: structType, addressable: addressable}
if fields, ok := t.jsonSchemaFieldsCache[key]; ok {
return fields
}
if t.jsonSchemaFieldsCache == nil {
t.jsonSchemaFieldsCache = make(map[jsonSchemaFieldsKey]map[string]jsonSchemaField)
}
fields := t.computeJSONSchemaFields(structType, addressable)
t.jsonSchemaFieldsCache[key] = fields
return fields
}

func (t *structTransformer) computeJSONSchemaFields(structType reflect.Type, addressable bool) map[string]jsonSchemaField {
candidates := make(map[string][]jsonSchemaField)
visited := make(map[reflect.Type]bool)
level := []*embeddedJSONStruct{{structType: structType, count: 1, addressable: addressable}}
Expand Down
83 changes: 83 additions & 0 deletions transformers/json_schema_fields_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,51 @@ type schemaTestUnexportedEmbed struct {
Exported string `json:"exported"`
}

type SchemaTestGeneratedClient struct {
ID string `json:"id"`
Name *string `json:"name,omitempty"`
ForceSendFields []string `json:"-"`
AdditionalProperties map[string]any
}

func (v SchemaTestGeneratedClient) MarshalJSON() ([]byte, error) {
object := map[string]any{"id": v.ID}
if v.Name != nil {
object["name"] = *v.Name
}
for key, value := range v.AdditionalProperties {
object[key] = value
}
return json.Marshal(object)
}

type SchemaTestServerResponse struct {
StatusCode int `json:"statusCode"`
}

type SchemaTestGeneratedResponse struct {
SchemaTestServerResponse `json:"-"`
ID string `json:"id"`
explicitFields *SchemaTestSecret //nolint:unused
}

func (v SchemaTestGeneratedResponse) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]string{"id": v.ID})
}

type SchemaTestPaymentSource struct {
ID string `json:"id"`
Object string `json:"object"`
Card *SchemaTestSecret `json:"-"`
}

func (v SchemaTestPaymentSource) MarshalJSON() ([]byte, error) {
if v.Card != nil {
return json.Marshal(v.Card)
}
return json.Marshal(v.ID)
}

func TestJSONTypeSchemaMatchesEncoder(t *testing.T) {
tests := []struct {
name string
Expand Down Expand Up @@ -266,6 +311,33 @@ func TestJSONTypeSchemaMatchesEncoder(t *testing.T) {
}{},
want: `{"port":"json"}`,
},
{
name: "keeps the tagged fields of a type with MarshalJSON",
testStruct: struct {
Item struct {
Client SchemaTestGeneratedClient `json:"client"`
} `json:"item"`
}{},
want: `{"client":{"id":"utf8","name":"utf8"}}`,
},
{
name: "keeps the tagged fields of a type with MarshalJSON and hidden metadata",
testStruct: struct {
Item struct {
Response SchemaTestGeneratedResponse `json:"response"`
} `json:"item"`
}{},
want: `{"response":{"id":"utf8"}}`,
},
{
name: "uses json for a type with MarshalJSON that hides a struct variant",
testStruct: struct {
Item struct {
Source SchemaTestPaymentSource `json:"source"`
} `json:"item"`
}{},
want: `{"source":"json"}`,
},
{
name: "uses json for a type with a pointer MarshalJSON",
testStruct: struct {
Expand Down Expand Up @@ -439,6 +511,17 @@ func TestJSONTypeSchemaKeysMatchMarshalledValue(t *testing.T) {
}{}
},
},
{
name: "types with MarshalJSON and json tags",
column: func() any {
return &struct {
Item struct {
Client SchemaTestGeneratedClient
Response SchemaTestGeneratedResponse
}
}{}
},
},
{
name: "diamond of embedded structs",
column: func() any { return schemaTestDiamondColumn(8) },
Expand Down
40 changes: 39 additions & 1 deletion transformers/struct.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type structTransformer struct {
jsonSchemaNameTransformer NameTransformer

maxJSONTypeSchemaDepth int
jsonSchemaFieldsCache map[jsonSchemaFieldsKey]map[string]jsonSchemaField
}

func isFieldStruct(reflectType reflect.Type) bool {
Expand Down Expand Up @@ -301,6 +302,35 @@ func implementsInterface(t reflect.Type, iface reflect.Type, addressable bool) b
return t.Implements(iface) || (addressable && reflect.PointerTo(t).Implements(iface))
}

func jsonTagName(field reflect.StructField) string {
name, _, _ := strings.Cut(field.Tag.Get("json"), ",")
return name
}

func hasJSONTaggedField(t reflect.Type) bool {
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
if name := jsonTagName(field); field.IsExported() && name != "" && name != "-" {
return true
}
}
return false
}

func hasHiddenStructField(t reflect.Type) bool {
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fieldType := field.Type
if fieldType.Kind() == reflect.Pointer {
fieldType = fieldType.Elem()
}
if field.IsExported() && !field.Anonymous && jsonTagName(field) == "-" && fieldType.Kind() == reflect.Struct {
return true
}
}
return false
}

func ownMarshalerSchema(t reflect.Type, addressable bool) (string, bool) {
switch t.Kind() {
case reflect.Struct, reflect.Map, reflect.Slice:
Expand All @@ -309,6 +339,9 @@ func ownMarshalerSchema(t reflect.Type, addressable bool) (string, bool) {
}
switch {
case implementsInterface(t, jsonMarshalerType, addressable):
if t.Kind() == reflect.Struct && hasJSONTaggedField(t) && !hasHiddenStructField(t) {
return "", false
}
return types.ExtensionTypes.JSON.String(), true
case implementsInterface(t, textMarshalerType, addressable):
return arrow.BinaryTypes.String.String(), true
Expand Down Expand Up @@ -344,7 +377,12 @@ func (t *structTransformer) fieldToJSONSchema(field reflect.StructField, depth i
switch normalizedField.Elem().Kind() {
case reflect.Struct:
fieldsMap := make(map[string]any)
for name, schemaField := range t.jsonSchemaFields(normalizedField.Elem().Type(), addressable) {
structType := normalizedField.Elem().Type()
taggedOnly := implementsInterface(structType, jsonMarshalerType, addressable)
for name, schemaField := range t.jsonSchemaFields(structType, addressable) {
if taggedOnly && !schemaField.tagged {
continue
}
structField := schemaField.field
columnType, err := t.getColumnType(structField)
if err != nil {
Expand Down
Loading