diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..be88519 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @oapi-codegen/maintainers diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..0d0b1c9 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1 @@ +_extends: .github diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..53043de --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,24 @@ +name: Build project +on: [ push, pull_request ] +jobs: + build: + name: Build + runs-on: ubuntu-latest + strategy: + fail-fast: false + # perform matrix testing to give us an earlier insight into issues with different versions of supported major versions of Go + matrix: + version: + - "1.20" + - "1.21" + steps: + - name: Check out source code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.version }} + + - name: Test + run: make test diff --git a/.github/workflows/generate.yml b/.github/workflows/generate.yml new file mode 100644 index 0000000..2ecde00 --- /dev/null +++ b/.github/workflows/generate.yml @@ -0,0 +1,27 @@ +name: Ensure generated files are up-to-date +on: [ push, pull_request ] +jobs: + build: + name: Build + runs-on: ubuntu-latest + strategy: + fail-fast: false + # perform matrix testing to give us an earlier insight into issues with different versions of supported major versions of Go + matrix: + version: + - "1.20" + - "1.21" + steps: + - name: Check out source code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.version }} + + - name: Run `make generate` + run: make generate + + - name: Check for no untracked files + run: git status && git diff-index --quiet HEAD -- diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..6865776 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,24 @@ +name: Lint project +on: [push, pull_request] +jobs: + build: + name: Build + runs-on: ubuntu-latest + strategy: + fail-fast: false + # perform matrix testing to give us an earlier insight into issues with different versions of supported major versions of Go + matrix: + version: + - "1.20" + - "1.21" + steps: + - name: Check out source code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.version }} + + - name: Run `make lint-ci` + run: make lint-ci diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 0000000..2c2c3b5 --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,25 @@ +name: Release Drafter + +on: + push: + branches: + - main + workflow_dispatch: {} + +permissions: + contents: read + +jobs: + update_release_draft: + permissions: + contents: write + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: release-drafter/release-drafter@v5 + with: + name: next + tag: next + version: next + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/tidy.yml b/.github/workflows/tidy.yml new file mode 100644 index 0000000..234b7e0 --- /dev/null +++ b/.github/workflows/tidy.yml @@ -0,0 +1,27 @@ +name: Ensure `go mod tidy` has been run +on: [ push, pull_request ] +jobs: + build: + name: Build + runs-on: ubuntu-latest + strategy: + fail-fast: false + # perform matrix testing to give us an earlier insight into issues with different versions of supported major versions of Go + matrix: + version: + - "1.20" + - "1.21" + steps: + - name: Check out source code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.version }} + + - name: Install `tidied` + run: go install gitlab.com/jamietanna/tidied@latest + + - name: Check for no untracked files + run: tidied -verbose diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5e56e04 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/bin diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..441ec72 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright 2024 oapi-codegen + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e2dd389 --- /dev/null +++ b/Makefile @@ -0,0 +1,28 @@ +GOBASE=$(shell pwd) +GOBIN=$(GOBASE)/bin + +help: + @echo "This is a helper makefile for oapi-codegen" + @echo "Targets:" + @echo " test: run all tests" + @echo " tidy tidy go mod" + @echo " lint run linting" + +$(GOBIN)/golangci-lint: + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOBIN) v1.55.2 + +.PHONY: tools +tools: $(GOBIN)/golangci-lint + +lint: tools + git ls-files go.mod '**/*go.mod' -z | xargs -0 -I{} bash -xc 'cd $$(dirname {}) && $(GOBIN)/golangci-lint run ./...' + +lint-ci: tools + git ls-files go.mod '**/*go.mod' -z | xargs -0 -I{} bash -xc 'cd $$(dirname {}) && $(GOBIN)/golangci-lint run ./... --out-format=github-actions --timeout=5m' + +test: + git ls-files go.mod '**/*go.mod' -z | xargs -0 -I{} bash -xc 'cd $$(dirname {}) && go test -cover ./...' + +tidy: + @echo "tidy..." + git ls-files go.mod '**/*go.mod' -z | xargs -0 -I{} bash -xc 'cd $$(dirname {}) && go mod tidy' diff --git a/README.md b/README.md new file mode 100644 index 0000000..09e352d --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# oapi-codegen/nullable + +> An implementation of a `Nullable` type for JSON bodies, indicating whether the field is absent, set to null, or set to a value + +Unlike other known implementations, this makes it possible to both marshal and unmarshal the value, as well as represent all three states: + +- the field is _not set_ +- the field is _explicitly set to null_ +- the field is _explicitly set to a given value_ + +And can be embedded in structs, for instance with the following definition: + +```go +obj := struct { + // RequiredID is a required, nullable field + RequiredID nullable.Nullable[int] `json:"id"` + // RequiredID is an optional, nullable field + OptionalString *nullable.Nullable[string] `json:"optionalString,omitempty"` +}{} +``` + +## Usage + +> [!IMPORTANT] +> Although this project is under the [oapi-codegen org](https://github.com/oapi-codegen) for the `oapi-codegen` OpenAPI-to-Go code generator, this is intentionally released as a separate, standalone library which can be used by other projects. + +First, add to your project with: + +```sh +go get github.com/oapi-codegen/nullable +``` + +Check out the examples in [the package documentation on pkg.go.dev](https://pkg.go.dev/github.com/oapi-codegen/nullable) for more details. + +## Credits + +- [KumanekoSakura](https://github.com/KumanekoSakura), [via](https://github.com/golang/go/issues/64515#issuecomment-1841057182) +- [Sebastien Guilloux], [via](https://github.com/sebgl/nullable/) + +As well as contributions from: + +- [Jamie Tanna](https://www.jvt.me) +- [Ashutosh Kumar](https://github.com/sonasingh46) + +## License + +Licensed under the Apache-2.0 license. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..99b48c1 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/oapi-codegen/nullable + +go 1.20 diff --git a/internal/test/go.mod b/internal/test/go.mod new file mode 100644 index 0000000..67ee0a0 --- /dev/null +++ b/internal/test/go.mod @@ -0,0 +1,16 @@ +module github.com/oapi-codegen/nullable/internal/test + +go 1.20 + +replace github.com/oapi-codegen/nullable => ../../ + +require ( + github.com/oapi-codegen/nullable v0.0.0-00010101000000-000000000000 + github.com/stretchr/testify v1.8.4 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/internal/test/go.sum b/internal/test/go.sum new file mode 100644 index 0000000..fa4b6e6 --- /dev/null +++ b/internal/test/go.sum @@ -0,0 +1,10 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/test/nullable_test.go b/internal/test/nullable_test.go new file mode 100644 index 0000000..745e89d --- /dev/null +++ b/internal/test/nullable_test.go @@ -0,0 +1,88 @@ +package nullable_test + +import ( + "encoding/json" + "testing" + + "github.com/oapi-codegen/nullable" + + "github.com/stretchr/testify/require" +) + +type Obj struct { + Foo nullable.Nullable[string] `json:"foo,omitempty"` // note "omitempty" is important for fields that are optional +} + +func TestNullable(t *testing.T) { + // --- parsing from json and serializing back to JSON + + // -- case where there is an actual value + data := `{"foo":"bar"}` + // deserialize from json + myObj := parse(data, t) + require.Equal(t, myObj, Obj{Foo: nullable.Nullable[string]{true: "bar"}}) + require.False(t, myObj.Foo.IsNull()) + require.True(t, myObj.Foo.IsSpecified()) + value, err := myObj.Foo.Get() + require.NoError(t, err) + require.Equal(t, "bar", value) + // serialize back to json: leads to the same data + require.Equal(t, data, serialize(myObj, t)) + + // -- case where no value is specified: parsed from JSON + data = `{}` + // deserialize from json + myObj = parse(data, t) + require.Equal(t, myObj, Obj{Foo: nil}) + require.False(t, myObj.Foo.IsNull()) + require.False(t, myObj.Foo.IsSpecified()) + _, err = myObj.Foo.Get() + require.ErrorContains(t, err, "value is not specified") + // serialize back to json: leads to the same data + require.Equal(t, data, serialize(myObj, t)) + + // -- case where the specified value is explicitly null + data = `{"foo":null}` + // deserialize from json + myObj = parse(data, t) + require.Equal(t, myObj, Obj{Foo: nullable.Nullable[string]{false: ""}}) + require.True(t, myObj.Foo.IsNull()) + require.True(t, myObj.Foo.IsSpecified()) + _, err = myObj.Foo.Get() + require.ErrorContains(t, err, "value is null") + // serialize back to json: leads to the same data + require.Equal(t, data, serialize(myObj, t)) + + // --- building objects from a Go client + + // - case where there is an actual value + myObj = Obj{} + myObj.Foo.Set("bar") + require.Equal(t, `{"foo":"bar"}`, serialize(myObj, t)) + + // - case where the value should be unspecified + myObj = Obj{} + // do nothing: unspecified by default + require.Equal(t, `{}`, serialize(myObj, t)) + // explicitly mark unspecified + myObj.Foo.SetUnspecified() + require.Equal(t, `{}`, serialize(myObj, t)) + + // - case where the value should be null + myObj = Obj{} + myObj.Foo.SetNull() + require.Equal(t, `{"foo":null}`, serialize(myObj, t)) +} + +func parse(data string, t *testing.T) Obj { + var myObj Obj + err := json.Unmarshal([]byte(data), &myObj) + require.NoError(t, err) + return myObj +} + +func serialize(o Obj, t *testing.T) string { + data, err := json.Marshal(o) + require.NoError(t, err) + return string(data) +} diff --git a/nullable.go b/nullable.go new file mode 100644 index 0000000..01b2a45 --- /dev/null +++ b/nullable.go @@ -0,0 +1,94 @@ +package nullable + +import ( + "bytes" + "encoding/json" + "errors" +) + +// Nullable is a generic type, which implements a field that can be one of three states: +// +// - field is not set in the request +// - field is explicitly set to `null` in the request +// - field is explicitly set to a valid value in the request +// +// Nullable is intended to be used with JSON marshalling and unmarshalling. +// +// Internal implementation details: +// +// - map[true]T means a value was provided +// - map[false]T means an explicit null was provided +// - nil or zero map means the field was not provided +// +// If the field is expected to be optional, add the `omitempty` JSON tags. Do NOT use `*Nullable`! +// +// Adapted from https://github.com/golang/go/issues/64515#issuecomment-1841057182 +type Nullable[T any] map[bool]T + +// Get retrieves the underlying value, if present, and returns an error if the value was not present +func (t Nullable[T]) Get() (T, error) { + var empty T + if t.IsNull() { + return empty, errors.New("value is null") + } + if !t.IsSpecified() { + return empty, errors.New("value is not specified") + } + return t[true], nil +} + +// Set sets the underlying value to a given value +func (t *Nullable[T]) Set(value T) { + *t = map[bool]T{true: value} +} + +// IsNull indicate whether the field was sent, and had a value of `null` +func (t Nullable[T]) IsNull() bool { + _, foundNull := t[false] + return foundNull +} + +// SetNull indicate that the field was sent, and had a value of `null` +func (t *Nullable[T]) SetNull() { + var empty T + *t = map[bool]T{false: empty} +} + +// IsSpecified indicates whether the field was sent +func (t Nullable[T]) IsSpecified() bool { + return len(t) != 0 +} + +// SetUnspecified indicate whether the field was sent +func (t *Nullable[T]) SetUnspecified() { + *t = map[bool]T{} +} + +func (t Nullable[T]) MarshalJSON() ([]byte, error) { + // if field was specified, and `null`, marshal it + if t.IsNull() { + return []byte("null"), nil + } + + // if field was unspecified, and `omitempty` is set on the field's tags, `json.Marshal` will omit this field + + // otherwise: we have a value, so marshal it + return json.Marshal(t[true]) +} + +func (t *Nullable[T]) UnmarshalJSON(data []byte) error { + // if field is unspecified, UnmarshalJSON won't be called + + // if field is specified, and `null` + if bytes.Equal(data, []byte("null")) { + t.SetNull() + return nil + } + // otherwise, we have an actual value, so parse it + var v T + if err := json.Unmarshal(data, &v); err != nil { + return err + } + t.Set(v) + return nil +} diff --git a/nullable_example_test.go b/nullable_example_test.go new file mode 100644 index 0000000..e767ca4 --- /dev/null +++ b/nullable_example_test.go @@ -0,0 +1,371 @@ +package nullable_test + +import ( + "encoding/json" + "fmt" + + "github.com/oapi-codegen/nullable" +) + +func ExampleNullable_marshalRequired() { + obj := struct { + ID nullable.Nullable[int] `json:"id"` + }{} + + // when it's not set (by default) + b, err := json.Marshal(obj) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + fmt.Println("Unspecified:") + fmt.Printf(`JSON: %s`+"\n", b) + fmt.Println("---") + + // when it's not set (explicitly) + obj.ID.SetUnspecified() + + b, err = json.Marshal(obj) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + fmt.Println("Unspecified:") + fmt.Printf(`JSON: %s`+"\n", b) + fmt.Println("---") + + // when it's set explicitly to nil + obj.ID.SetNull() + + b, err = json.Marshal(obj) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + fmt.Println("Null:") + fmt.Printf(`JSON: %s`+"\n", b) + fmt.Println("---") + + // when it's set explicitly to the zero value + var v int + obj.ID.Set(v) + + b, err = json.Marshal(obj) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + fmt.Println("Zero value:") + fmt.Printf(`JSON: %s`+"\n", b) + fmt.Println("---") + + // when it's set explicitly to a specific value + v = 12345 + obj.ID.Set(v) + + b, err = json.Marshal(obj) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + fmt.Println("Value:") + fmt.Printf(`JSON: %s`+"\n", b) + fmt.Println("---") + + // Output: + // Unspecified: + // JSON: {"id":0} + // --- + // Unspecified: + // JSON: {"id":0} + // --- + // Null: + // JSON: {"id":null} + // --- + // Zero value: + // JSON: {"id":0} + // --- + // Value: + // JSON: {"id":12345} + // --- +} + +func ExampleNullable_marshalOptional() { + obj := struct { + ID nullable.Nullable[int] `json:"id,omitempty"` + }{} + + // when it's not set (by default) + b, err := json.Marshal(obj) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + fmt.Println("Unspecified:") + fmt.Printf(`JSON: %s`+"\n", b) + fmt.Println("---") + + // when it's not set (explicitly) + obj.ID.SetUnspecified() + + b, err = json.Marshal(obj) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + fmt.Println("Unspecified:") + fmt.Printf(`JSON: %s`+"\n", b) + fmt.Println("---") + + // when it's set explicitly to nil + obj.ID.SetNull() + + b, err = json.Marshal(obj) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + fmt.Println("Null:") + fmt.Printf(`JSON: %s`+"\n", b) + fmt.Println("---") + + // when it's set explicitly to the zero value + var v int + obj.ID.Set(v) + + b, err = json.Marshal(obj) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + fmt.Println("Zero value:") + fmt.Printf(`JSON: %s`+"\n", b) + fmt.Println("---") + + // when it's set explicitly to a specific value + v = 12345 + obj.ID.Set(v) + + b, err = json.Marshal(obj) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + fmt.Println("Value:") + fmt.Printf(`JSON: %s`+"\n", b) + fmt.Println("---") + + // Output: + // Unspecified: + // JSON: {} + // --- + // Unspecified: + // JSON: {} + // --- + // Null: + // JSON: {"id":null} + // --- + // Zero value: + // JSON: {"id":0} + // --- + // Value: + // JSON: {"id":12345} + // --- +} + +func ExampleNullable_unmarshalRequired() { + obj := struct { + Name nullable.Nullable[string] `json:"name"` + }{} + + // when it's not set + err := json.Unmarshal([]byte(` + { + } + `), &obj) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + fmt.Println("Unspecified:") + fmt.Printf("obj.Name.IsSpecified(): %v\n", obj.Name.IsSpecified()) + fmt.Printf("obj.Name.IsNull(): %v\n", obj.Name.IsNull()) + fmt.Println("---") + + // when it's set explicitly to nil + err = json.Unmarshal([]byte(` + { + "name": null + } + `), &obj) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + fmt.Println("Null:") + fmt.Printf("obj.Name.IsSpecified(): %v\n", obj.Name.IsSpecified()) + fmt.Printf("obj.Name.IsNull(): %v\n", obj.Name.IsNull()) + fmt.Println("---") + + // when it's set explicitly to the zero value + err = json.Unmarshal([]byte(` + { + "name": "" + } + `), &obj) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + fmt.Println("Zero value:") + fmt.Printf("obj.Name.IsSpecified(): %v\n", obj.Name.IsSpecified()) + fmt.Printf("obj.Name.IsNull(): %v\n", obj.Name.IsNull()) + val, err := obj.Name.Get() + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + fmt.Printf("obj.Name.Get(): %#v \n", val) + fmt.Println("---") + + // when it's set explicitly to a specific value + err = json.Unmarshal([]byte(` + { + "name": "foo" + } + `), &obj) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + + fmt.Println("Value:") + fmt.Printf("obj.Name.IsSpecified(): %v\n", obj.Name.IsSpecified()) + fmt.Printf("obj.Name.IsNull(): %v\n", obj.Name.IsNull()) + val, err = obj.Name.Get() + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + fmt.Printf("obj.Name.Get(): %#v \n", val) + fmt.Println("---") + + // Output: + // Unspecified: + // obj.Name.IsSpecified(): false + // obj.Name.IsNull(): false + // --- + // Null: + // obj.Name.IsSpecified(): true + // obj.Name.IsNull(): true + // --- + // Zero value: + // obj.Name.IsSpecified(): true + // obj.Name.IsNull(): false + // obj.Name.Get(): "" + // --- + // Value: + // obj.Name.IsSpecified(): true + // obj.Name.IsNull(): false + // obj.Name.Get(): "foo" + // --- +} + +func ExampleNullable_unmarshalOptional() { + obj := struct { + // Note that there is no pointer for nullable.Nullable when it's + Name nullable.Nullable[string] `json:"name,omitempty"` + }{} + + // when it's not set + err := json.Unmarshal([]byte(` + { + } + `), &obj) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + fmt.Println("Unspecified:") + fmt.Printf("obj.Name.IsSpecified(): %v\n", obj.Name.IsSpecified()) + fmt.Printf("obj.Name.IsNull(): %v\n", obj.Name.IsNull()) + fmt.Println("---") + + // when it's set explicitly to nil + err = json.Unmarshal([]byte(` + { + "name": null + } + `), &obj) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + fmt.Println("Null:") + fmt.Printf("obj.Name.IsSpecified(): %v\n", obj.Name.IsSpecified()) + fmt.Printf("obj.Name.IsNull(): %v\n", obj.Name.IsNull()) + fmt.Println("---") + + // when it's set explicitly to the zero value + err = json.Unmarshal([]byte(` + { + "name": "" + } + `), &obj) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + fmt.Println("Zero value:") + fmt.Printf("obj.Name.IsSpecified(): %v\n", obj.Name.IsSpecified()) + fmt.Printf("obj.Name.IsNull(): %v\n", obj.Name.IsNull()) + val, err := obj.Name.Get() + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + fmt.Printf("obj.Name.Get(): %#v \n", val) + fmt.Println("---") + + // when it's set explicitly to a specific value + err = json.Unmarshal([]byte(` + { + "name": "foo" + } + `), &obj) + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + + fmt.Println("Value:") + fmt.Printf("obj.Name.IsSpecified(): %v\n", obj.Name.IsSpecified()) + fmt.Printf("obj.Name.IsNull(): %v\n", obj.Name.IsNull()) + val, err = obj.Name.Get() + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + fmt.Printf("obj.Name.Get(): %#v \n", val) + fmt.Println("---") + + // Output: + // Unspecified: + // obj.Name.IsSpecified(): false + // obj.Name.IsNull(): false + // --- + // Null: + // obj.Name.IsSpecified(): true + // obj.Name.IsNull(): true + // --- + // Zero value: + // obj.Name.IsSpecified(): true + // obj.Name.IsNull(): false + // obj.Name.Get(): "" + // --- + // Value: + // obj.Name.IsSpecified(): true + // obj.Name.IsNull(): false + // obj.Name.Get(): "foo" + // --- +}