Graphql: How to marshal / unmarshal bson.ObjectId?

Created on 8 Jul 2018  路  3Comments  路  Source: graphql-go/graphql

I'm using graphlql-go in conjunction with mgo and would like to know how to setup the _id field.

// Customer describes a customer
type Customer struct {
    ID         bson.ObjectId   `json:"_id,omitempty" bson:"_id,omitempty"`
    CreatedAt  time.Time       `json:"createdAt" bson:"createdAt"`
    LastSeenAt time.Time       `json:"lastSeenAt" bson:"lastSeenAt"`
}


// CustomerType is the graphql representation of a Customer
var CustomerType = graphql.NewObject(graphql.ObjectConfig{
    Name: "Customer",
    Fields: graphql.Fields{
        "_id": &graphql.Field{
            Type: graphql.String,
        },
        "createdAt": &graphql.Field{
            Type: graphql.DateTime,
        },
        "lastSeenAt": &graphql.Field{
            Type: graphql.DateTime,
        },
    },
})

If I now query all customers with:

{
  customers {
    _id
    createdAt
    lastSeenAt
  }
}

I receive this answer:

{
  "data": {
    "customers": [
      {
        "_id": "ObjectIdHex(\"5b3f4d00d73eb0194e672a8a\")",
        "createdAt": "0001-01-01T00:00:00Z",
        "lastSeenAt": "0001-01-01T00:00:00Z"
      }
    ]
  }
}

How can I tell graphql-go to use the .Hex() method to stringify the ObjectId when marshaling JSON?

question

Most helpful comment

Hi @michelalbers, thanks a lot for using the lib.

That's correct, in your use case, you have to let know graphql-go about the bson.ObjectId type, and the way we can do that is via a custom scalar.

I went ahead and wrote a working example with a custom BSON scalar:

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "time"

    "github.com/globalsign/mgo"
    "github.com/globalsign/mgo/bson"
    "github.com/graphql-go/graphql"
    "github.com/graphql-go/graphql/language/ast"
)

type Customer struct {
    ID         bson.ObjectId `json:"_id" bson:"_id"`
    CreatedAt  time.Time     `json:"createdAt" bson:"createdAt"`
    LastSeenAt time.Time     `json:"lastSeenAt" bson:"lastSeenAt"`
}

var BSON = graphql.NewScalar(graphql.ScalarConfig{
    Name:        "BSON",
    Description: "The `bson` scalar type represents a BSON Object.",
    // Serialize serializes `bson.ObjectId` to string.
    Serialize: func(value interface{}) interface{} {
        switch value := value.(type) {
        case bson.ObjectId:
            return value.Hex()
        case *bson.ObjectId:
            v := *value
            return v.Hex()
        default:
            return nil
        }
    },
    // ParseValue parses GraphQL variables from `string` to `bson.ObjectId`.
    ParseValue: func(value interface{}) interface{} {
        switch value := value.(type) {
        case string:
            return bson.ObjectIdHex(value)
        case *string:
            return bson.ObjectIdHex(*value)
        default:
            return nil
        }
        return nil
    },
    // ParseLiteral parses GraphQL AST to `bson.ObjectId`.
    ParseLiteral: func(valueAST ast.Value) interface{} {
        switch valueAST := valueAST.(type) {
        case *ast.StringValue:
            return bson.ObjectIdHex(valueAST.Value)
        }
        return nil
    },
})

var CustomerType = graphql.NewObject(graphql.ObjectConfig{
    Name: "Customer",
    Fields: graphql.Fields{
        "_id": &graphql.Field{
            Type: BSON,
        },
        "createdAt": &graphql.Field{
            Type: graphql.DateTime,
        },
        "lastSeenAt": &graphql.Field{
            Type: graphql.DateTime,
        },
    },
})

func main() {
    session, err := mgo.Dial("localhost")
    if err != nil {
        log.Fatal(err)
    }
    db := session.DB("testdb")
    schema, err := graphql.NewSchema(graphql.SchemaConfig{
        Query: graphql.NewObject(graphql.ObjectConfig{
            Name: "Query",
            Fields: graphql.Fields{
                "customers": &graphql.Field{
                    Type: graphql.NewList(CustomerType),
                    Args: graphql.FieldConfigArgument{
                        "_id": &graphql.ArgumentConfig{
                            Type: BSON,
                        },
                    },
                    Resolve: func(p graphql.ResolveParams) (interface{}, error) {
                        // _id := p.Args["_id"]
                        // log.Printf("_id args: %+v", _id)
                        var customers []Customer
                        if err := db.C("customers").Find(nil).All(&customers); err != nil {
                            return nil, err
                        }
                        return customers, nil
                    },
                },
            },
        }),
        Types: []graphql.Type{BSON},
    })
    if err != nil {
        log.Fatal(err)
    }
    query := "{ customers { _id, createdAt, lastSeenAt } }"
    /*
        queryWithVariable := `
            query($id: BSON) {
                customers(_id: $id) {
                    _id
                    createdAt
                    lastSeenAt
                }
            }
        `
        queryWithArgument := `
            query {
                customers(_id: "5b42ba57289fb278f2a4a13f") {
                    _id
                    createdAt
                    lastSeenAt
                }
            }
        `
    */
    result := graphql.Do(graphql.Params{
        Schema:        schema,
        RequestString: query,
        VariableValues: map[string]interface{}{
            "id": "5b42ba57289fb278f2a4a13f",
        },
    })
    if len(result.Errors) > 0 {
        log.Fatal(result)
    }
    b, err := json.Marshal(result)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(string(b))
}
(mgo)-> go run main.go | jq
{
  "data": {
    "customers": [
      {
        "_id": "5b42ba57289fb278f2a4a13f",
        "createdAt": "2018-07-09T01:28:55.711Z",
        "lastSeenAt": "2018-07-09T01:28:55.711Z"
      },
      {
        "_id": "5b42ba5a289fb278f2a4a140",
        "createdAt": "2018-07-09T01:28:58.929Z",
        "lastSeenAt": "2018-07-09T01:28:58.929Z"
      }
    ]
  }
}

All 3 comments

Hi @michelalbers, thanks a lot for using the lib.

That's correct, in your use case, you have to let know graphql-go about the bson.ObjectId type, and the way we can do that is via a custom scalar.

I went ahead and wrote a working example with a custom BSON scalar:

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "time"

    "github.com/globalsign/mgo"
    "github.com/globalsign/mgo/bson"
    "github.com/graphql-go/graphql"
    "github.com/graphql-go/graphql/language/ast"
)

type Customer struct {
    ID         bson.ObjectId `json:"_id" bson:"_id"`
    CreatedAt  time.Time     `json:"createdAt" bson:"createdAt"`
    LastSeenAt time.Time     `json:"lastSeenAt" bson:"lastSeenAt"`
}

var BSON = graphql.NewScalar(graphql.ScalarConfig{
    Name:        "BSON",
    Description: "The `bson` scalar type represents a BSON Object.",
    // Serialize serializes `bson.ObjectId` to string.
    Serialize: func(value interface{}) interface{} {
        switch value := value.(type) {
        case bson.ObjectId:
            return value.Hex()
        case *bson.ObjectId:
            v := *value
            return v.Hex()
        default:
            return nil
        }
    },
    // ParseValue parses GraphQL variables from `string` to `bson.ObjectId`.
    ParseValue: func(value interface{}) interface{} {
        switch value := value.(type) {
        case string:
            return bson.ObjectIdHex(value)
        case *string:
            return bson.ObjectIdHex(*value)
        default:
            return nil
        }
        return nil
    },
    // ParseLiteral parses GraphQL AST to `bson.ObjectId`.
    ParseLiteral: func(valueAST ast.Value) interface{} {
        switch valueAST := valueAST.(type) {
        case *ast.StringValue:
            return bson.ObjectIdHex(valueAST.Value)
        }
        return nil
    },
})

var CustomerType = graphql.NewObject(graphql.ObjectConfig{
    Name: "Customer",
    Fields: graphql.Fields{
        "_id": &graphql.Field{
            Type: BSON,
        },
        "createdAt": &graphql.Field{
            Type: graphql.DateTime,
        },
        "lastSeenAt": &graphql.Field{
            Type: graphql.DateTime,
        },
    },
})

func main() {
    session, err := mgo.Dial("localhost")
    if err != nil {
        log.Fatal(err)
    }
    db := session.DB("testdb")
    schema, err := graphql.NewSchema(graphql.SchemaConfig{
        Query: graphql.NewObject(graphql.ObjectConfig{
            Name: "Query",
            Fields: graphql.Fields{
                "customers": &graphql.Field{
                    Type: graphql.NewList(CustomerType),
                    Args: graphql.FieldConfigArgument{
                        "_id": &graphql.ArgumentConfig{
                            Type: BSON,
                        },
                    },
                    Resolve: func(p graphql.ResolveParams) (interface{}, error) {
                        // _id := p.Args["_id"]
                        // log.Printf("_id args: %+v", _id)
                        var customers []Customer
                        if err := db.C("customers").Find(nil).All(&customers); err != nil {
                            return nil, err
                        }
                        return customers, nil
                    },
                },
            },
        }),
        Types: []graphql.Type{BSON},
    })
    if err != nil {
        log.Fatal(err)
    }
    query := "{ customers { _id, createdAt, lastSeenAt } }"
    /*
        queryWithVariable := `
            query($id: BSON) {
                customers(_id: $id) {
                    _id
                    createdAt
                    lastSeenAt
                }
            }
        `
        queryWithArgument := `
            query {
                customers(_id: "5b42ba57289fb278f2a4a13f") {
                    _id
                    createdAt
                    lastSeenAt
                }
            }
        `
    */
    result := graphql.Do(graphql.Params{
        Schema:        schema,
        RequestString: query,
        VariableValues: map[string]interface{}{
            "id": "5b42ba57289fb278f2a4a13f",
        },
    })
    if len(result.Errors) > 0 {
        log.Fatal(result)
    }
    b, err := json.Marshal(result)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(string(b))
}
(mgo)-> go run main.go | jq
{
  "data": {
    "customers": [
      {
        "_id": "5b42ba57289fb278f2a4a13f",
        "createdAt": "2018-07-09T01:28:55.711Z",
        "lastSeenAt": "2018-07-09T01:28:55.711Z"
      },
      {
        "_id": "5b42ba5a289fb278f2a4a140",
        "createdAt": "2018-07-09T01:28:58.929Z",
        "lastSeenAt": "2018-07-09T01:28:58.929Z"
      }
    ]
  }
}

Thank you so much @chris-ramon for pointing out and implementing it! Maybe we should include this in the library itself since I think this is a very common case.

Somehow the type switch in Serialize does not work as expected. It falls through in the default case and when I print the type via:

println(reflect.TypeOf(value))

I receive a memory address (0x14a46a0,0xc4202321e0). I'm a Go beginner so I probably miss something. Do you have an idea what it might be?

Edit: Querying by ID via your example works as expected :)

Edit 2: My models are in a separate package. If I put them in the same package as the schema and scalar definition it works. I can probably refactor that - the question of "why?!" remains. Can someone shed some light?

Edit 3: FOUND IT! :) I was importing mgo/v2/bson in one file and globalsign/mgo/bson in the other. Now its working perfectly fine. Thank you so much!

Glad to know you have the BSON scalar setup & working @michelalbers.
Also added a custom scalar type example for further reference under, PR: https://github.com/graphql-go/graphql/pull/344

Was this page helpful?
0 / 5 - 0 ratings

Related issues

januszm picture januszm  路  6Comments

schaze picture schaze  路  4Comments

Fruchtgummi picture Fruchtgummi  路  4Comments

tobyjsullivan picture tobyjsullivan  路  5Comments

conord33 picture conord33  路  6Comments