Skip to content

Commit b72bb2a

Browse files
Jamie Tannajamietanna
authored andcommitted
feat(output-options): allow using omitzero with prefer-skip-optional-pointer
One of the key things `oapi-codegen` does is to use an "optional pointer", following idiomatic Go practices, to indicate that a field/type is optional. As noted in oapi-codegen#1899, now Go 1.24+ has the `omitzero` JSON tag for marshaling only when a type is not its zero value. As we support generating `omitzero` JSON tags via the `x-omitzero` extension, we can build on top of this to consider allowing the use of `omitzero` when using the newly added global Output Option, `prefer-skip-optional-pointer` to tune this behaviour. This introduces a new Output Option, `prefer-skip-optional-pointer-with-omitzero`, which mirrors behaviour from `prefer-skip-optional-pointer`, but produces an `omitzero` flag for any skipped pointer types, to not marshal the type if the zero value. This allows folks using Go 1.24+ to much more ergonomically work with different (optional) types, while allowing `omitzero` to simplify (un)marshalling. Right now, we don't have a straightforward means of enforcing/warning that `oapi-codegen` is being generated into a non-Go-1.24+ project, so we'll only document the behaviour. Closes oapi-codegen#1899.
1 parent c6493ab commit b72bb2a

12 files changed

Lines changed: 440 additions & 0 deletions

File tree

configuration-schema.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,11 @@
243243
"description": "Allows defining at a global level whether to omit the pointer for a type to indicate that the field/type is optional. This is the same as adding `x-go-type-skip-optional-pointer` to each field (manually, or using an OpenAPI Overlay). A field can set `x-go-type-skip-optional-pointer: false` to still require the optional pointer.",
244244
"default": false
245245
},
246+
"prefer-skip-optional-pointer-with-omitzero": {
247+
"type": "boolean",
248+
"description": "When using `prefer-skip-optional-pointer`, generate the `omitzero` JSON tag for types that would have had an optional pointer. This is the same as adding `x-omitzero` to each field (manually, or using an OpenAPI Overlay). A field can set `x-omitzero: false` to disable the `omitzero` JSON tag.\nNOTE that this requires Go 1.24+.\nNOTE that this must be used alongside `prefer-skip-optional-pointer`, otherwise makes no difference.",
249+
"default": false
250+
},
246251
"prefer-skip-optional-pointer-on-container-types": {
247252
"type": "boolean",
248253
"description": "Allows disabling the generation of an 'optional pointer' for an optional field that is a container type (such as a slice or a map), which ends up requiring an additional, unnecessary, `... != nil` check. A field can set `x-go-type-skip-optional-pointer: false` to still require the optional pointer.",
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
SHELL:=/bin/bash
2+
3+
YELLOW := \e[0;33m
4+
RESET := \e[0;0m
5+
6+
GOVER := $(shell go env GOVERSION)
7+
GOMINOR := $(shell bash -c "cut -f2 -d. <<< $(GOVER)")
8+
9+
define execute-if-go-124
10+
@{ \
11+
if [[ 24 -le $(GOMINOR) ]]; then \
12+
$1; \
13+
else \
14+
echo -e "$(YELLOW)Skipping task as you're running Go v1.$(GOMINOR).x which is < Go 1.24, which this module requires$(RESET)"; \
15+
fi \
16+
}
17+
endef
18+
19+
lint:
20+
$(call execute-if-go-124,$(GOBIN)/golangci-lint run ./...)
21+
22+
lint-ci:
23+
24+
$(call execute-if-go-124,$(GOBIN)/golangci-lint run ./... --output.text.path=stdout --timeout=5m)
25+
26+
generate:
27+
$(call execute-if-go-124,go generate ./...)
28+
29+
test:
30+
$(call execute-if-go-124,go test -cover ./...)
31+
32+
tidy:
33+
$(call execute-if-go-124,go mod tidy)
34+
35+
tidy-ci:
36+
$(call execute-if-go-124,tidied -verbose)
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
openapi: "3.0.0"
2+
info:
3+
version: 1.0.0
4+
title: prefer-skip-optional-pointer-with-omitzero
5+
components:
6+
schemas:
7+
Client:
8+
type: object
9+
required:
10+
- name
11+
properties:
12+
name:
13+
description: This field is required, so will never have an optional pointer, nor `omitzero`.
14+
type: string
15+
id:
16+
description: This field is optional, but the `prefer-skip-optional-pointer` Output Option ensures that this should not have an optional pointer. However, it will receive `omitzero`.
17+
type: number
18+
ClientWithExtension:
19+
type: object
20+
required:
21+
- name
22+
properties:
23+
name:
24+
description: This field is required, so will never have an optional pointer, nor `omitzero`.
25+
type: string
26+
id:
27+
description: This field is optional, but the `prefer-skip-optional-pointer` Output Option ensures that this should not have an optional pointer. However, it will receive `omitzero`.
28+
type: number
29+
pointer_id:
30+
type: number
31+
description: This field should have an optional pointer, as the field-level definition of `x-go-type-skip-optional-pointer` overrides the `prefer-skip-optional-pointer` Output Option. This will also not receive an `omitzero`.
32+
# NOTE that this overrides the global preference
33+
x-go-type-skip-optional-pointer: false
34+
no_omit:
35+
type: number
36+
description: This field is optional, but the `prefer-skip-optional-pointer` Output Option ensures that this should not have an optional pointer. This will not receive `omitzero`, as the field-level definition of `x-omitzero` overrides the `prefer-skip-optional-pointer-with-omitzero` Output Option.
37+
# NOTE that this overrides the global preference
38+
x-omitzero: false
39+
NestedType:
40+
type: object
41+
properties:
42+
client:
43+
description: This field is optional, but the `prefer-skip-optional-pointer` Output Option ensures that this should not have an optional pointer.
44+
$ref: '#/components/schemas/Client'
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# yaml-language-server: $schema=../../../configuration-schema.json
2+
package: preferskipoptionalpointerwithomitzero
3+
output: gen.go
4+
generate:
5+
models: true
6+
output-options:
7+
# to make sure that all types are generated, even if they're unreferenced
8+
skip-prune: true
9+
prefer-skip-optional-pointer: true
10+
prefer-skip-optional-pointer-with-omitzero: true

examples/output-options/preferskipoptionalpointerwithomitzero/gen.go

Lines changed: 33 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package preferskipoptionalpointerwithomitzero
2+
3+
import (
4+
"encoding/json"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestClient(t *testing.T) {
12+
t.Run("zero value (empty string) on Name is not omitted", func(t *testing.T) {
13+
client := Client{
14+
Name: "",
15+
}
16+
17+
b, err := json.Marshal(client)
18+
require.NoError(t, err)
19+
20+
assert.True(t, jsonContainsKey(b, "name"))
21+
})
22+
23+
t.Run("value on Name is not omitted", func(t *testing.T) {
24+
client := Client{
25+
Name: "some value",
26+
}
27+
28+
b, err := json.Marshal(client)
29+
require.NoError(t, err)
30+
31+
assert.True(t, jsonContainsKey(b, "name"))
32+
})
33+
34+
t.Run("zero value (0.0) on Id is omitted (as `omitempty` and/or `omitzero` flags it as empty)", func(t *testing.T) {
35+
client := Client{
36+
Id: 0.0,
37+
}
38+
39+
b, err := json.Marshal(client)
40+
require.NoError(t, err)
41+
42+
assert.False(t, jsonContainsKey(b, "id"))
43+
})
44+
45+
t.Run("value on Id is not omitted", func(t *testing.T) {
46+
client := Client{
47+
Id: 3.142,
48+
}
49+
50+
b, err := json.Marshal(client)
51+
require.NoError(t, err)
52+
53+
assert.True(t, jsonContainsKey(b, "id"))
54+
})
55+
}
56+
57+
func TestNestedType(t *testing.T) {
58+
t.Run("zero value (empty struct) on Client is omitted", func(t *testing.T) {
59+
nestedType := NestedType{
60+
Client: Client{},
61+
}
62+
63+
b, err := json.Marshal(nestedType)
64+
require.NoError(t, err)
65+
66+
assert.False(t, jsonContainsKey(b, "client"))
67+
})
68+
69+
t.Run("value on Client is not omitted", func(t *testing.T) {
70+
nestedType := NestedType{
71+
Client: Client{
72+
Name: "foo",
73+
},
74+
}
75+
76+
b, err := json.Marshal(nestedType)
77+
require.NoError(t, err)
78+
79+
assert.True(t, jsonContainsKey(b, "client"))
80+
})
81+
}
82+
83+
// jsonContainsKey checks if the given JSON object contains the specified key at the top level.
84+
func jsonContainsKey(b []byte, key string) bool {
85+
var m map[string]any
86+
if err := json.Unmarshal(b, &m); err != nil {
87+
return false
88+
}
89+
_, ok := m[key]
90+
return ok
91+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package preferskipoptionalpointerwithomitzero
2+
3+
//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen -config cfg.yaml api.yaml
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
module github.com/oapi-codegen/oapi-codegen/v2/examples/output-options/preferskipoptionalpointerwithomitzero
2+
3+
go 1.24
4+
5+
replace github.com/oapi-codegen/oapi-codegen/v2 => ../../../
6+
7+
require (
8+
github.com/oapi-codegen/oapi-codegen/v2 v2.0.0-00010101000000-000000000000
9+
github.com/stretchr/testify v1.10.0
10+
)
11+
12+
require (
13+
github.com/davecgh/go-spew v1.1.1 // indirect
14+
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect
15+
github.com/getkin/kin-openapi v0.128.0 // indirect
16+
github.com/go-openapi/jsonpointer v0.21.0 // indirect
17+
github.com/go-openapi/swag v0.23.0 // indirect
18+
github.com/invopop/yaml v0.3.1 // indirect
19+
github.com/josharian/intern v1.0.0 // indirect
20+
github.com/mailru/easyjson v0.7.7 // indirect
21+
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
22+
github.com/perimeterx/marshmallow v1.1.5 // indirect
23+
github.com/pmezard/go-difflib v1.0.0 // indirect
24+
github.com/speakeasy-api/jsonpath v0.6.0 // indirect
25+
github.com/speakeasy-api/openapi-overlay v0.10.2 // indirect
26+
github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect
27+
golang.org/x/mod v0.17.0 // indirect
28+
golang.org/x/text v0.20.0 // indirect
29+
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
30+
gopkg.in/yaml.v2 v2.4.0 // indirect
31+
gopkg.in/yaml.v3 v3.0.1 // indirect
32+
)

0 commit comments

Comments
 (0)