Khan / genqlient

a truly type-safe Go GraphQL client
MIT License
1.02k stars 99 forks source link

Using `# @genqlient(typename: "Webhook")` adds `json:"-"` to generated struct (?) #319

Closed aawnu closed 3 months ago

aawnu commented 4 months ago

Describe the bug I have multiple queries that use the same fragment, in which case I have given all the queries the same # @genqlient(typename: "Webhook") tak to make sure it unanimous across my code base. However, the struct that is made simply imports the fields from the auto generated struct and then adds json:"-" to it, meaning I lose serialization.

To Reproduce

# This is a scraped version of the query I am running
# search params etc is not necessary for this issue
query GetWebhook {
  webhooks {
    # @genqlient(typename: "Webhook")
    data {
      ...webhookFields
    }
  }
}

fragment webhookFields on Webhook {
  id
  name
  source
  enabled
  createdAt
}

# The following code is generated; 
# and I can not seem to figure out how to get rid of json:"-"
type Webhook struct {
    webhookFields `json:"-"`
}

type webhookFields struct {
    Id            string       `json:"id"`
    Name          string       `json:"name"`
    Source        string       `json:"source"`
    Enabled       bool         `json:"enabled"`
    CreatedAt     time.Time    `json:"createdAt"`
}

Expected behavior I expect the exported struct to be serializable as if I used the raw struct generated without the tag

genqlient version github.com/Khan/genqlient v0.6.0

benjaminjkraft commented 4 months ago

Can you explain more about what isn't working and why json:"-" is causing problems? Are you seeing actual serialization errors? Anytime genqlient adds that it should also add appropriate MarshalJSON/UnmarshalJSON (in this case to Webhook) to ensure everything serializes correctly.

aawnu commented 4 months ago

When I look at the code there is a marshal and unmashal, but when I use the Webhook inside Temporal (passes json between activities) then it does not encode the json.

// Webhook includes the requested fields of the GraphQL type Webhook.
type Webhook struct {
    webhookFields `json:"-"`
}

// GetId returns Webhook.Id, and is useful for accessing the field via an interface.
func (v *Webhook) GetId() string { return v.webhookFields.Id }

// GetName returns Webhook.Name, and is useful for accessing the field via an interface.
func (v *Webhook) GetName() string { return v.webhookFields.Name }

// GetSource returns Webhook.Source, and is useful for accessing the field via an interface.
func (v *Webhook) GetSource() string { return v.webhookFields.Source }

// GetEnabled returns Webhook.Enabled, and is useful for accessing the field via an interface.
func (v *Webhook) GetEnabled() bool { return v.webhookFields.Enabled }

// GetCreatedAt returns Webhook.CreatedAt, and is useful for accessing the field via an interface.
func (v *Webhook) GetCreatedAt() time.Time { return v.webhookFields.CreatedAt }

func (v *Webhook) UnmarshalJSON(b []byte) error {

    if string(b) == "null" {
        return nil
    }

    var firstPass struct {
        *Webhook
        graphql.NoUnmarshalJSON
    }
    firstPass.Webhook = v

    err := json.Unmarshal(b, &firstPass)
    if err != nil {
        return err
    }

    err = json.Unmarshal(
        b, &v.webhookFields)
    if err != nil {
        return err
    }
    return nil
}

type __premarshalWebhook struct {
    Id string `json:"id"`

    Name string `json:"name"`

    Source string `json:"source"`

    Enabled bool `json:"enabled"`

    CreatedAt time.Time `json:"createdAt"`
}

func (v *Webhook) MarshalJSON() ([]byte, error) {
    premarshaled, err := v.__premarshalJSON()
    if err != nil {
        return nil, err
    }
    return json.Marshal(premarshaled)
}

func (v *Webhook) __premarshalJSON() (*__premarshalWebhook, error) {
    var retval __premarshalWebhook

    retval.Id = v.webhookFields.Id
    retval.Name = v.webhookFields.Name
    retval.Source = v.webhookFields.Source
    retval.Enabled = v.webhookFields.Enabled
    retval.CreatedAt = v.webhookFields.CreatedAt
    return &retval, nil
}

If I write a hack for it, it does not trigger

func main() {
    webhook := graphql.Webhook{}
    webhook.Name = "Cookie"
    webhook.CreatedAt = time.Now()

    _, j := json.Marshal(webhook)
    log.Printf("json=%s", j)
}
> go run main.go
2024/02/21 14:23:19 json=%!s(<nil>)
aawnu commented 4 months ago

I can not explain why it works, but if I remove the json:"-" then it seems to serialize fine within Temporal. I did however make my own serialization class to ensure it would not end up not serializing on future changes (regenerations).

benjaminjkraft commented 4 months ago

Ah, you probably want to pass a pointer (&webhook) so the receiver works. That tends to be the convention for anything JSON, although maybe we could make it a value receiver so it doesn't matter? That might require some research, not sure if there are perf or complexity issues.

aawnu commented 4 months ago

I do unfortunately not determine how temporal passes data between workers/activities, so right now I use the middleware struct (basically a copy of the root/internal struct). It would be interesting if this somehow could be worked out :D

benjaminjkraft commented 4 months ago

Can you not pass the pointer in? Does Temporal dereference the pointer?

In any case, I think you'll need to give a more complete example of what your code is doing with Temporal for us to help. In the code you listed, the fix is to pass a pointer. If that's not what your code is really doing, many other problems could be at play, whether with genqlient or Temporal.

aawnu commented 3 months ago

I'm sorry for never getting back to you, a few things have changed and I will soon no longer have access to this code I am working on. Due to its work relation I am unable to make you a replicate of my issue. If I encounter it in my own project then I will return with example repo if possible. I will close my issue due to this change of accessibility.