Per spec:
If the value null was provided for an input object field, and the field鈥檚 type is not a non鈥恘ull type, an entry in the coerced unordered map is given the value null. In other words, there is a semantic difference between the explicitly provided value null versus having not provided a value.
Right now its not possible (as far as I know) to differentiate between null and not having provided a value.
input UpdateUserInput {
id: ID!
name: String
age: Int
}
produces
type UpdateUserInput struct {
ID string `json:"id"`
Name *string `json:"name"`
Age *int `json:"age"`
}
If the user now submits the input with only id and name, I don't know what to do with the age. Did the user want to clear his age (i.e. set it to null) or did he not want to update it (i.e. not specified)?
I think the previous statement in the spec is probable the relevant part here:
If no default value is provided and the input object field鈥檚 type is non鈥恘ull, an error should be thrown. Otherwise, if the field is not required, then no entry is added to the coerced unordered map.
Unfortunately this is probably part of the spec that is a little too coupled to it's JS reference implementation. We want the type safety of a generated struct from your input type, but there's no immediately obvious way to differentiate between a null value and a value that was not provided.
I'm not 100% sure of a good solution here. Part of me thinks trying to differentiate like this might be a bit of a design smell, but I also think your use-case of unset vs ignore is valid. @vektah any thoughts?
There is no way to represent undefined on a struct, to get the raw unordered map instead of a struct you can ask for it:
models:
UpdateUserInput:
model: map[string]interface{}
But you will need to deal with casting yourself.
A better api is probably one that moves away from random field updates, and has separate updateAge, updateName mutations where you can put validation and buisiness logic.
A few options:
type UpdateUserInput struct {
ID string `json:"id"`
Name NullableString `json:"name"`
}
type NullableString struct {
String *string
Touched bool
}
Then if Touched == false its unspecified.
type UpdateUserInput struct {
ID string `json:"id"`
Name NullableString `json:"name"`
}
func (i *UpdateUserInput) Name(v *string) {
i.Name = NullableString{v, true}
}
(or any other way of keeping tracked if it was unspecified)
rawArgs in ResolverContextCurrently only the parsed Args are available in ResolverContext. This could be changed to also include rawArgs. Then I don't have to do any casting myself and can look up if the field was specified.
You can already do 1 but you need to implement the custom scalars interface.
You can already get raw args by doing https://github.com/99designs/gqlgen/issues/505#issuecomment-456999324.
2 is interesting, but lets track it separatly.
A better api is probably one that moves away from random field updates, and has separate updateAge, updateName mutations where you can put validation and buisiness logic.
I don't think thats a good assumption to make. Coarse vs fine mutations, different people will see this differently. Shouldn't a library like this cater to both and rely on the spec?
You can already do 1 but you need to implement the custom scalars interface.
I don't actually see how this can be achieved while keeping the scalar in the schema as String. As far as I can tell from the docs, I can either add a custom type (don't want that as it should be String) or for a type I don't control, but then I would have to return a string in UnmarshalString. Is there another way thats not documented?
You can already get raw args by doing #505 (comment).
But I loose everything else. Thats why I think it would be better to access it via the context.
2 is interesting, but lets track it separatly.
If that is something you guys are interested in (see #506), I would create a PR for it.
I don't actually see how this can be achieved while keeping the scalar in the schema as String. As far as I can tell from the docs, I can either add a custom type (don't want that as it should be String) or for a type I don't control, but then I would have to return a string in UnmarshalString. Is there another way thats not documented?
Scalars dont need to be strings, they are just undivisible in the graph. You should be able to create a NullableString as you describe by implementing the marshaler interface.
But I loose everything else. Thats why I think it would be better to access it via the context.
What do you lose? In order to work with raw args you're going to need to typecast.
Scalars dont need to be strings, they are just undivisible in the graph. You should be able to create a NullableString as you describe by implementing the marshaler interface.
When I try this
type UpdateUserInput struct {
ID string `json:"id"`
Name NullableString `json:"name"`
}
type NullableString struct{}
func (s *NullableString) UnmarshalGQL(v interface{}) error {
return nil
}
func (s NullableString) MarshalGQL(w io.Writer) {
}
it returns an error
field has wrong type: *string is not compatible with a.b.c.NullableString.
As I said I want to keep String in the schema itself and not use a custom scalar. Is there something I am missing? Maybe you could please show an example so it becomes clearer?
What do you lose? In order to work with raw args you're going to need to typecast.
If I have to use map[string]interface{} for UpdateUserInput I loose the whole type safety and automatic unmarshaling. Whats the reason for not including rawArgs in the ResolverContext so they are easily gettable?
Ah, part of the work in 0.8 is to allow multiple backing go types for a given graph type. This will allow you to add a custom string implementation to be used for that one context.
If I have to use map[string]interface{} for UpdateUserInput I loose the whole type safety and automatic unmarshaling.
You are never going to get type safety from a map[string]interface{}, in context or otherwise.
Whats the reason for not including rawArgs in the ResolverContext so they are easily gettable?
Its already on resolver context as Args, the only difference between that map and what gets passed to the method is a final type assertion.
You are never going to get type safety from a map[string]interface{}, in context or otherwise.
Its already on resolver context as Args, the only difference between that map and what gets passed to the method is a final type assertion.
Thats not what I mean. The current Args in the ResolverContext are already cast to their respective structs. So in order to check if the field was specified I could look it up in rawArgs if it was specified. I can't do that with the current Args. And therefor, the type safety of rawArgs does not matter, because I get the usual struct, but have additionally the rawArgs to look up if each field was actually specified.
Anyway that was just one of my proposed solutions. Being able to have a custom string implementation (1. solution) or setters (2. solution) would solve the problem in a nicer way.
Anyway that was just one of my proposed solutions. Being able to have a custom string implementation (1. solution) or setters (2. solution) would solve the problem in a nicer way.
Ok, I'm going to close this in favour of input setters.
Did any of you ever got this working ? I took the tutorial and expaned it with a nullablestring like:
schema:
type Todo {
id: ID!
text: NullableString
done: Boolean!
user: User!
}
type User {
id: ID!
name: String!
}
type Query {
todos: [Todo!]!
}
input NewTodo {
text: NullableString
userId: String!
}
type Mutation {
createTodo(input: NewTodo!): Todo!
}
scalar NullableString
todo.go:
type NullableString struct {
String *string
Touched bool
}
func (n *NullableString) UnmarshalGQL(v interface{}) error {
n.Touched = true
s, ok := v.(string)
if !ok {
return fmt.Errorf("Not a string, got %v", v)
}
n.String = &s
return nil
}
func (n NullableString) MarshalGQL(w io.Writer) {
if !n.Touched {
fmt.Fprintf(w, "nill") // just to test with a value
} else if n.Touched && n.String == nil {
fmt.Fprintf(w, "null")
} else {
fmt.Fprintf(w, "%s", *n.String)
}
}
type Todo struct {
ID string
Text NullableString
Done bool
UserID string
}
If I send this request:
mutation createTodo {
createTodo(input:{userId:"1"}) {
user {
id
}
text
done
}
}
I would expect that text is null .. its not its 'nill'
If I send:
mutation createTodo {
createTodo(input:{text:null, userId:"1"}) {
user {
id
}
text
done
}
}
I would expect the nullablestring to have set Touched but its not .. I get nill again.
It seems that if the input value has the value null the UnmarshalGQL is never called. Can anyone tell me what I'm missing ?
Just for reference, it doesn't seem like a custom implementation of the String scalar helps with this as UnmarshalGQL isn't called when the field is explicitly set to null in the input; i.e. it still isn't possible to determine if the field is absent or if it is explicitly set to null.
So what's the recommended way to do this? As @jszwedko mentioned the NullString does not work and it seems that #506 is also not implemented yet, which would mean there's no solution at the moment.
I also had an idea where the implementation could use a double pointer (ugly, but it would work) like **string to differentiate between null and undefined
This seems to work and results in types which are explicitly set by the user!
yes, ok := requestContext.Variables["input"].(map[string]interface{})
fmt.Println(ok)
for key, _ := range yes {
fmt.Println(key)
}
Still looking for a workaround for this. Best we've come up with is to never omit fields so we know that a null is really meant to be removing a value. But that makes more work on the client side to send field values other than what needs to be changed. I tried accessing Variables as @RichardLindhout suggested, but it's always empty.
I generate code which return whitelisted sqlboiler update keys based in the input so I know which fields are set and which not. https://github.com/web-ridge/gqlgen-sqlboiler
func getInputFromContext(ctx context.Context, key string) map[string]interface{} {
requestContext := graphql.GetRequestContext(ctx)
m, ok := requestContext.Variables[key].(map[string]interface{})
if !ok {
fmt.Println("can not get input from context")
}
return m
}
func {{ .Name|go }}ToBoilerWhitelist(ctx context.Context, extraColumns ...string) boil.Columns {
input := getInputFromContext(ctx, "input")
columnsWhichAreSet := []string{}
for key, _ := range input {
switch key {
{{ range $field := .Fields -}}
case "{{ $field.CamelCaseName }}":
columnsWhichAreSet = append(columnsWhichAreSet, models.{{ $model.BoilerName|go }}Columns.{{- $field.BoilerName|go }})
{{ end -}}
}
}
columnsWhichAreSet = append(columnsWhichAreSet, extraColumns...)
return boil.Whitelist(columnsWhichAreSet...)
}
@RichardLindhout I tried that, but I just get an empty map in RequestContext.Variables.
@danilobuerger, have you come up with a workable alternative? Haven't seen anything from you recently.
@Schparky I think maybe because I'm specifying that the input should be called input maybe you don't have that
input := getInputFromContext(ctx, "input")
Based on

https://blog.apollographql.com/designing-graphql-mutations-e09de826ed97
@RichardLindhout , thanks, I have that. The difference comes before, though. If I print the Variables field, like:
requestContext := graphql.GetRequestContext(ctx)
fmt.Printf("------ vars: %+v\n", requestContext.Variables)
I get this
------ vars: map[]
I'm still on 0.10.1. Perhaps 0.10.2 changes this? I've seen that GetRequestContext is deprecated in favor of GetOperationContext but not sure if that has anything to do with what I'm seeing.
I think it probably does, will update in the future to the new gqlgen to test this.
@RichardLindhout are you still using your solution with getInputFromContext()?
Or is there something simpler today?
Yes:
https://github.com/web-ridge/utils-go/blob/main/boilergql/input.go#L11
So only these fields get force updated and the other ones are skipped.
Most helpful comment
Just for reference, it doesn't seem like a custom implementation of the
Stringscalar helps with this asUnmarshalGQLisn't called when the field is explicitly set tonullin the input; i.e. it still isn't possible to determine if the field is absent or if it is explicitly set tonull.