gqlgen icon indicating copy to clipboard operation
gqlgen copied to clipboard

Request for omitempty on json tag for models

Open apoggi-carecloud opened this issue 5 years ago • 10 comments

Instead of having to manually define models with the 'omitempty' json tag, it would be great if we could pass that through the yaml config file.

apoggi-carecloud avatar Jul 09 '19 19:07 apoggi-carecloud

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] avatar Sep 07 '19 20:09 stale[bot]

Any update on this? Or any workaround about the omitempty tag?

iliasgal avatar Oct 14 '19 11:10 iliasgal

I just wrote the omitempty tags via code generation to the files

apoggi-carecloud avatar Oct 14 '19 14:10 apoggi-carecloud

@apoggi-carecloud Does you write a plugins to add omitempty tag? Or you wrote the omitempty tags to the files after code generation?

zhulinwei avatar Feb 08 '20 09:02 zhulinwei

Omit per field could be a be a usefull feature - if the models are marshalled for internal usage. The generated marshaling to match a graphql query returns empty things. It has no external effect if a struct attribute is marked with omitempty tag, if the attribute is marked with an ! in the schema and therefore is not generated as a pointer. I prefer using no pointers especially for strings (to avoid the nasty two step instantiation) and mark everything with a !. I reuse the generated models for typesafe db ops and add typesafe validation (via ozzo) to them in another file, which is not overwritten by the generator. usually i add the omitempty tags to every model (and do other additions and custom renamings (like adding a dot in the json name etc.) via a small script like (example only for adding all omitempty tags), which is automatically called after every new model generation:

path := "models/models_gen.go"
	read, err := ioutil.ReadFile(path)
	if err != nil {
		panic(err)
	}

	newContents = strings.Replace(newContents, "\"`", ",omitempty\"`", -1)

	err = ioutil.WriteFile(path, []byte(newContents), 0)
	if err != nil {
		panic(err)
	}

RobinKamps avatar Feb 15 '20 21:02 RobinKamps

Let's re-open this - omitempty is important for marshaling structs to JSON to send in a request. Lots of APIs have a default value for a field, but if you marshal a struct with an empty field right now, it'll use the default value (like an empty string), which will override the API's default.

KenG98 avatar Feb 27 '20 16:02 KenG98

I only need use model_gen hook = https://gqlgen.com/recipes/modelgen-hook/

let's say you have gqlgen.yml in root dir, then create a file called runner_model_gen.go

in runner_model_gen.go:

package main

import (
	"fmt"
	"github.com/99designs/gqlgen/api"
	"github.com/99designs/gqlgen/codegen/config"
	"github.com/99designs/gqlgen/plugin/modelgen"
	"os"
	"strings"
)

func mutateHook(b *modelgen.ModelBuild) *modelgen.ModelBuild {
	for _, model := range b.Models {
		for _, field := range model.Fields {
                       // this is the logic to add omitempty
			omit := strings.TrimSuffix(field.Tag, `"`)
			field.Tag = fmt.Sprintf(`%v,omitempty"`, omit)
		}
	}
	return b
}

func main() {
	cfg, err := config.LoadConfigFromDefaultLocations()
	if err != nil {
		fmt.Fprintln(os.Stderr, "failed to load config", err.Error())
		os.Exit(2)
	}

	p := modelgen.Plugin{
		MutateHook: mutateHook,
	}

	err = api.Generate(cfg,
		api.NoPlugins(),
		api.AddPlugin(&p),
	)
	if err != nil {
		fmt.Fprintln(os.Stderr, err.Error())
		os.Exit(3)
	}
}

then run them go run runner_model_gen.go

check your model_gen.go then u can see the result, enjoy:)

harmnot avatar Jun 12 '20 14:06 harmnot

I was looking for this as a feature of gqlgen and stumbled upon this issue. And I found myself wondering why it might not have been implemented (personally I like omitempty). But when I thought about it, when you have omitempty, it would be odd for someone to have a query:

{
  people {
    name
    nickname
  }
}

And get back something like:

{
    "data": {
        "people": [
            {
                "name": "Stephen",
                "nickname": "Steve"
            },
            {
                "name": "Joe"
            }
        ]
    }
}

instead of something like

{
    "data": {
        "people": [
            {
                "name": "Stephen",
                "nickname": "Steve"
            },
            {
                "name": "Joe",
                "nickname": null
            }
        ]
    }
}

Especially given that your query directly asks for it. So maybe in the graphql world, it is good to return back the empty value of the field.

~~But then again, some empty values, like for bools and ints, could be deceptive by returning falses and 0s~~ (nvm, that is what the pointers are for lol)

Thoughts?

thestephenstanton avatar Jul 18 '20 16:07 thestephenstanton

This causes default values for input types not to be applied.

input Foo {
  principalType: PrincipalType = User
}

enum PrincipalType {
  User
  Org
}
  • unmarshal code checks for existence of the field in the payload, which it does. with a "null" . so the default value does nt get initialized
type Foo struct {
	PrincipalType *PrincipalType `json:"principalType" yaml:"principalType"`
}

//...

func (ec *executionContext) unmarshalInputFoo(ctx context.Context, obj interface{}) (model.Foo, error) {
	var it model.Foo
	var asMap = obj.(map[string]interface{})

	if _, present := asMap["principalType"]; !present {
		asMap["principalType"] = "User"
	}

sanjeevchopra avatar Dec 19 '20 03:12 sanjeevchopra

I'm re-using the models internally and I wanted the possibility to add omitempty since I'm marshalling the objects into maps to send to other APIs.

However for my scenario it can be achieve with a simple solution.

Minor example:

func cleanPayload(properties *model.UserProperties) (map[string]interface{}, error) {
	marshaled, err := json.Marshal(payload)
	if err != nil {
		return nil, err
	}

	mapped := make(map[string]interface{})
	err = json.Unmarshal(marshaled, &mapped)
	if err != nil {
		return nil, err
	}

	for key, val := range mapped {
		if val == nil {
			delete(mapped, key)
		}
	}

	return mapped, nil
}

It's working for us, hope it helps.

kelvne avatar Mar 01 '21 08:03 kelvne