Search code examples
gographqlgqlgen

Unable to use UUID for ID types within gqlgen's GraphQL models


I'm trying to use a UUID type (from module github.com/gofrs/uuid) within Go models. I do usually define models manually, unless I know they won't be augmented. This is what I currently have:

package model

import "github.com/gofrs/uuid"

type Category struct {
    ID          uuid.UUID `json:"id"`
    Name        string    `json:"name"`
    Description string    `json:"description"`
    Picture     string    `json:"picture,omitempty"`
}
# GraphQL Schema

...

type Category {
  id: ID!
  name: String!
  description: String!
  picture: String
}

The gqlgen configuration file looks like:

autobind:
  - <module-id>/path/to/model

...

models:
  ID:
    model:
      - github.com/99designs/gqlgen/graphql.ID
      - github.com/gofrs/uuid.UUID
  Int:
    model:
      - github.com/99designs/gqlgen/graphql.Int
      - github.com/99designs/gqlgen/graphql.Int32
      - github.com/99designs/gqlgen/graphql.Int64

...

schema:
  - graph/*.graphql

struct_tag: json

With this setup, I get this error message:

validation failed: packages.Load: /path/to/application/graph/prelude.generated.go:2177:17: ec.unmarshalInputID undefined (type *executionContext has no field or method unmarshalInputID)
/path/to/application/graph/prelude.generated.go:2182:12: ec._ID undefined (type *executionContext has no field or method _ID)

exit status 1
graph/resolver.go:3: running "go": exit status 1

Is there a way to use the UUID type "natively" for IDs without resorting in string types and doing the auto-conversion manually?

I was under the assumption that this general use case was covered by the framework 🤔


Updates

I went ahead and send a pull request for this work. I think this should be something most people will benefit from these days, where UUIDs are used (or adopted) more often.


Solution

  • This implementation should work (with some adjustments around packages names, identifiers, etc.):

    // uuid.go
    
    package graphql
    
    import (
        "errors"
        "io"
    
        "github.com/gofrs/uuid"
    )
    
    func MarshalUUID(t uuid.UUID) Marshaler {
        if t.IsNil() {
            return Null
        }
        return WriterFunc(func(w io.Writer) {
            _, _ = io.WriteString(w, t.String())
        })
    }
    
    func UnmarshalUUID(v interface{}) (uuid.UUID, error) {
        if str, ok := v.(string); ok {
            return uuid.FromString(str)
        }
        return uuid.Nil, errors.New("input must be an RFC-4122 formatted string")
    }
    

    I created a pull request with this exact implementation. If merged, this wouldn't be needed anymore because it will be a built-in behavior.