graphql icon indicating copy to clipboard operation
graphql copied to clipboard

Issue with embedded Struct response

Open SDkie opened this issue 8 years ago • 23 comments
trafficstars

type Address struct { City string json:"city" State string json:"state" }

type User struct { Name string json:"name" Email string json:"email" Address }

My resolver is returning User struct, And I am getting following response : { "name":"Jon", "email":"[email protected]", "city":null, "state":null }

Address is common struct shared by many sql tables. How can I fix city and state response?

SDkie avatar Nov 26 '16 10:11 SDkie

How is your GraphQL Object defined? What are the resolve functions specified?

bbuck avatar Nov 26 '16 17:11 bbuck

@bbuck : My sample code is here - https://play.golang.org/p/wAJA_MPGPU Response:

{
	"data": {
		"getUser": {
			"city": null,
			"email": "[email protected]",
			"name": "Jon",
			"state": null
		}
	}
}

SDkie avatar Nov 26 '16 18:11 SDkie

So that's kind of what I was thinking. You're not implementing a resolver for the "city" and "state" fields. I don't think it's really in the scope of the default resolver to search through nested struct fields, especially those that don't have a JSON struct tag specifying the name. Two solutions would be to either nest and "address" property, making the query:

{
  getUser {
    address {
      city
      state
    }
  }
}

The other is to implement the resolvers for those fields so they properly hunt the nested attribute correctly, you could make them generic functions by implementing an Addresser (or other named) interface and define a City() string and State() string so that your numerous data types can benefit from one Resolve function and just pass that as the Resolve in the object definition.

bbuck avatar Nov 26 '16 22:11 bbuck

@bbuck : Can I keep city, state, name and email at same level instead of nesting? Btw when I json.Marshal User object all fields are in the same level.

SDkie avatar Nov 27 '16 11:11 SDkie

If you want to, that's fine. Your resolver just needs to know where to get that data from.

Btw when I json.Marshal User object all fields are in the same level.

This is kind of fundamentally different. In this regard you're converting the entire struct into a JSON representation using struct tags to determine how to look at things. Here in GraphQL the default resolver is just piggybacking off the json struct tags to assume a field to resolve. However, if you define a field "city" on the User GraphQL object, then logically speaking the resolver should expect to find a field with json:"city" or named "City" on the struct User. I think it would be a significant amount of extra work if it hunted for the json:"city" on the structs fields, then any and all nested struct fields then looked for fields with the name --- etc.

At least that's how I see it. Resolving the object based on the config you can either match the the object definition to the struct or the struct to the object definition. But to deviate either layout from the other (like here with nested fields) you should be expected to provide a resolution function. Don't take my opinion as final, I just contribute here. Perhaps @chris-ramon or @sogko have their own ideas on how this should behave.

bbuck avatar Nov 28 '16 21:11 bbuck

I ran into this problem today, and I think that embedded structs should be handled in a more transparent way. I agree that this behavior should mimic json.Marshal and json.Unmarshal, and embedded properties should be part of the object rather than nested.

dominicbarnes avatar Aug 28 '17 01:08 dominicbarnes

You can work around this by implementing field-level resolve functions, but it is pretty inconvenient and not particularly intuitive/obvious.

"city": &graphql.Field{
	Type: graphql.String,
	Resolve: func(p graphql.ResolveParams) (interface{}, error) {
		m := p.Source.(User)
		return m.City, nil
	},
},

dominicbarnes avatar Aug 28 '17 01:08 dominicbarnes

It'd be great to have an easy way of converting structs to simple (optionally nested) key : value pairs. It's probably a very popular use case to just dump a struct into an JSON API response.

januszm avatar Jan 23 '18 14:01 januszm

I agree that the default resolver should automatically look for fields on nested structs.

alexflint avatar May 02 '18 01:05 alexflint

I'm also facing this with interfaces. I'm using embedded structs that contain all fields of the interface. When implementing a concrete type I need to implement the Resolve function as states above.

niondir avatar May 31 '18 10:05 niondir

Met the same issue, the default resolver didn't search for nested struct although json.Marshal works.

lynic avatar Jul 31 '18 07:07 lynic

Any news on this? Is it planned to search nested structs like json.Marshal does for default resolvers?

Or do I miss any way to somehow override the default resolver behaviour?

niondir avatar Oct 15 '18 14:10 niondir

@Niondir I think this pr will fix it, but it stays there for a while #371

imjma avatar Oct 16 '18 22:10 imjma

I don't think it's really in the scope of the default resolver to search through nested struct fields

Is this still the official position? I know how annoying nested structs are to deal with as library authors, but as consumers you just kind of expect nested structs to work everywhere.

abraithwaite avatar Mar 04 '19 15:03 abraithwaite

could we actually merge #371 ?

niondir avatar Sep 24 '19 18:09 niondir

Has this problem been solved?

zhulinwei avatar Jan 13 '20 09:01 zhulinwei

Running into this using jinzhu/gorm. GORM uses an embedded struct to DRY out common fields like ID, CreatedAt, UpdatedAt, and DeletedAt in database models. I'm having to create custom field revolvers for all of these fields on all types, which is just boilerplate noise. It would save many, many keystrokes to have graphql-go/graphql dive the embedded structs.

var UserType = gql.NewObject(gql.ObjectConfig{
	Name: "User",
	Fields: gql.Fields{
		"id": &gql.Field{
			Type: gql.Int,
			Resolve: func(p gql.ResolveParams) (i interface{}, err error) {
				user, ok := p.Source.(models.User)
				if !ok {
					return nil, errors.New("casting failed for user type")
				}
				return user.ID, nil
			},
		},
		"email":        &gql.Field{Type: gql.String},
		"dw_campaigns": &gql.Field{Type: gql.NewList(DwCampaignType)},
		"created_at": &gql.Field{
			Type: gql.DateTime,
			Resolve: func(p gql.ResolveParams) (i interface{}, err error) {
				user, ok := p.Source.(models.User)
				if !ok {
					return nil, errors.New("casting failed for user type")
				}
				return user.CreatedAt, nil
			},
		},
	},
})

vs

var UserType = gql.NewObject(gql.ObjectConfig{
	Name: "User",
	Fields: gql.Fields{
		"id":           &gql.Field{Type: gql.Int},
		"email":        &gql.Field{Type: gql.String},
		"dw_campaigns": &gql.Field{Type: gql.NewList(DwCampaignType)},
		"created_at":   &gql.Field{Type: gql.DateTime},
	},
})

emhohensee avatar Jan 16 '20 17:01 emhohensee

Since the JSON returned from a composite structure gives the correct values for a schema, there should be no issue. Instead, the problem persists and limit us very much. We must fix this.

limoli avatar Apr 28 '20 10:04 limoli

same issue. I populated the ValidateAccessTokenResponse struct with RESTful API response data. But the fields of embed structs are NOT resolved. Only the id field is resolved.

Go struct

type ResponseStatus struct {
	Success bool `json:"success"`
}
type User struct {
	Loginname string `json:"loginname"`
	AvatarURL string `json:"avatar_url"`
}
type ValidateAccessTokenResponse struct {
        ResponseStatus
	User
	ID string `json:"id"`
}
type UserDetail struct {
	User
	GithubUsername string        `json:"githubUsername"`
	CreateAt       string        `json:"create_at"`
	Score          int           `json:"score"`
	RecentTopics   []RecentTopic `json:"recent_topics"`
}

Got data:

{
  "data": {
    "validateAccessToken": {
      "avatar_url": null,
      "id": "58e104cdc669764920c00964",
      "loginname": null
    }
  }
}

Solution:

var UserBaseFields = graphql.Fields{
	"loginname": &graphql.Field{
		Type: graphql.String,
		Resolve: func(p graphql.ResolveParams) (interface{}, error) {
			switch t := p.Source.(type) {
                         // Both t.User.Loginname and t.Loginname are ok.
			case *models.ValidateAccessTokenResponse:
				return t.User.Loginname, nil
			case *models.UserDetail:
				return t.Loginname, nil
			default:
				return graphql.DefaultResolveFn(p)
			}
		}},
	"avatar_url": &graphql.Field{
		Type: graphql.String,
	},
}

var UserType = graphql.NewObject(graphql.ObjectConfig{
	Name:        "User",
	Description: "This respresents an user",
	Fields:      UserBaseFields,
})

var UserDetailType = graphql.NewObject(graphql.ObjectConfig{
	Name:        "UserDetail",
	Description: "This respresents an user detail",
	Fields: utils.MergeGraphqlFields(UserBaseFields, graphql.Fields{
		"githubUsername": &graphql.Field{Type: graphql.String},
		"create_at":      &graphql.Field{Type: graphql.String},
		"score":          &graphql.Field{Type: graphql.Int},
		"recent_topics":  &graphql.Field{Type: graphql.NewList(RecentTopicType)},
	}),
})

var AccessTokenValidationType = graphql.NewObject(graphql.ObjectConfig{
	Name:        "AccessTokenValidation",
	Description: "The response type for validating accessToken",
	Fields: utils.MergeGraphqlFields(UserBaseFields, graphql.Fields{
		"id": &graphql.Field{Type: graphql.NewNonNull(graphql.ID)},
	}),
})

Since UserBaseFields is used by UserDetailType and AccessTokenValidationType. The return value(and type) of these resolvers are different. So I need to use type cast to get the different field from p.Source.

mrdulin avatar Aug 11 '20 12:08 mrdulin

not resolved yet?

rifqimf12 avatar Sep 28 '20 07:09 rifqimf12

Noticed this issue when trying to resolve a field using a struct tag in an embedded field, like previous commenters have described in further detail. Thanks to the commenters here for providing some workarounds!

sheepwall avatar Nov 30 '20 22:11 sheepwall

This is still happening

dumim avatar Mar 14 '24 05:03 dumim

Yes, very annoying 🥲

taleeus avatar Mar 20 '24 11:03 taleeus