graphql icon indicating copy to clipboard operation
graphql copied to clipboard

Eliminating null fields in JSON output

Open atombender opened this issue 5 years ago • 10 comments

The resolver pipeline seems to like explicit null fields. For example, if you do something like query { things { id, name } }, you might end up with something like:

{
  "results": {
    "things": [
      {"id": "1", "name": null},
      {"id": "2", "name": null},
      {"id": "3", "name": null},
      {"id": "4", "name": null},
      {"id": "5", "name": "a"}
    ]
  }
}

but a frontend would generally be perfectly happy with:

{
  "results": {
    "things": [
      {"id": "1"},
      {"id": "2"},
      {"id": "3"},
      {"id": "4"},
      {"id": "5", "name": "a"}
    ]
  }
}

Considering the things result only, that's a space saving of 20%. For large result sets, the aggregate size of all null values can be considerable. A real-world result set of ours is 273KB with nulls, but 189KB without nulls, which is a space saving factor of 41%.

I could do a pass over our results to eliminate nulls (possibly expensive, reflection-heavy and error-prone) or use a JSON encoder that eliminates nulls or allows filtering (not sure if that exists). But it would be a lot simpler if the resolver code could have a mode where object keys that are null wouldn't be emitted in the first place. Thoughts?

atombender avatar Apr 03 '19 21:04 atombender

I think you can use struct_tags like this

type Thing struct {
  id int
  name string `json:",omitempty"'
}

esmaeilpour avatar Jun 04 '19 11:06 esmaeilpour

No, that would not let you distinguish between a null string and an empty string, which is not the same thing. Or deal with null structs.

atombender avatar Jun 04 '19 15:06 atombender

You use Pointer! *time.Time or *[]wtfarray .... This makes a accurate null-object.

Fruchtgummi avatar Oct 07 '19 15:10 Fruchtgummi

No idea what you meant by that, sorry. I want to avoid nulls ending up in the serialization.

atombender avatar Oct 07 '19 15:10 atombender

Well, exactly! Me too!

If you use omitempty for the string definition, send it and a key is empty, it will delete that key / value in your request. This pair is not available

Yes, good that far!

Of course, the other party has to deal with it. If you work with structures, then that should not be a problem.

But if you now have objects, arrays in your structures, then the receiver gets objects with value null. You can not declare an object within a structure with omitempty, you can do it, but that does not work. Unless the object within a structure is a pointer (*, &).

For example, the object time.Time:

type Thing struct {
  id int
  name string `json:",omitempty"'
  timer time.Time
}

For example, if you create this for the JSON string, the value of key timer will look like this:

0001-01-01T00:00:00Z

Other values will produce a null

If you change that easily, to:

type Thing struct {
  id int
  name string `json:",omitempty"'
  timer *time.Time
}

Then no time.Time object is transmitted. That's magic!

it may be that you still have to pack 'omitempty' behind it.

Fruchtgummi avatar Oct 08 '19 07:10 Fruchtgummi

Thanks, but two problems. First, an empty string is semantically not the same as null. Secondly, graphql-go does not use JSON tags for object definitions. Given:

type Thing struct {
  Name *string `json:"name,omitempty"
}

thingType := graphql.NewObject(graphql.ObjectConfig{
  Name: "Thing",
  Fields: graphql.Fields{
    "name": &graphql.Field{Type: graphql.String},
  },
  Resolve: func(p graphql.ResolveParams) (interface{}, error) {
    return Thing{Name: nil}, nil
  },
})
...

Then:

$ curl http://localhost:300/graphql?query={thing{name}}
{"thing":{"name":null}}

atombender avatar Oct 08 '19 15:10 atombender

Well, you are calling a function that should give you all the names?

Then there is always a name to or just Result 0!

If, for example, you are looking for a name, then GraphQL also makes a non-nullable field. So if you want that!

Model:

type Thing struct {
	Name string `json:"name,omitempty"  form:"name"`
}

Function:

func GetName(n string) (*models.Thing, error) {
	var t = &models.Thing{Name: n}
	return t, nil
}

GQLFunction:

"test": &graphql.Field{
			Type: UserType,
			Args: graphql.FieldConfigArgument{
				"name": &graphql.ArgumentConfig{
					Description: "Name String",
					Type:        graphql.NewNonNull(graphql.String), //
				},
			},
			Resolve: func(p graphql.ResolveParams) (interface{}, error) {

				name := p.Args["name"].(string)
				bname, err := controllers.GetName(name)

				if err != nil {
					log.Println(err)
					return nil, err
				}
				return bname, err
			},
		},

CURL http://localhost:5000/ql?query={test(name:"Keenefrombravia"){name}}

MyRESULT:

{"data":{"test":null},"errors":[{"message":"Cannot return null for non-nullable field Thing.name.","locations":[{"line":1,"column":31}],"path":["test","name"]}]}

Fruchtgummi avatar Oct 11 '19 15:10 Fruchtgummi

No, the field can be null. But there's no point in returning null data in JSON, because a missing field is by definition equivalent to null.

atombender avatar Oct 11 '19 15:10 atombender

ok, I understand!

I have just searched in an existing function for a user that does not exist!

{"data":{"user":null}}

now I am motivated!

Fruchtgummi avatar Oct 11 '19 15:10 Fruchtgummi

"but a frontend would generally be perfectly happy with:"

Frontender here, its not Graphql spec. See for example https://github.com/graphql/graphql-js/issues/731#issuecomment-284978418 and the size you are talking about is not so much when its gzipped. So in absolute numbers yes, in real numbers no. Also in frontend JS typeof null equals object, and we are very happy with null :)

maapteh avatar Oct 09 '20 08:10 maapteh