From 47f3d47de36629e257034b71a16d15f2e357acdf Mon Sep 17 00:00:00 2001 From: Frederic BIDON Date: Wed, 15 Apr 2026 23:09:23 +0200 Subject: [PATCH] feat: added alternate json name provider Signed-off-by: Frederic BIDON --- README.md | 12 +++++++++++- examples_test.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 2 +- go.sum | 4 ++-- options.go | 16 ++++++++++++++++ options_test.go | 22 ++++++++++++++++++++++ 6 files changed, 100 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 56c8e77..86604c5 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,14 @@ Or join our Slack channel: [![Slack Channel][slack-logo]![slack-badge]][slack-ur trailing "-", either pass a mutable array (`*[]T`) as the input document, or use the returned updated document instead. * types that implement the `JSONSetable` interface may not implement the mutation implied by the trailing "-" +* **2026-04-15** : added support for optional alternate JSON name providers + * for struct support the defaults might not suit all situations: there are known limitations + when it comes to handle untagged fields or embedded types. + * the default name provider in use is not fully aligned with go JSON stdlib + * exposed an option (or global setting) to change the provider that resolves a struct into json keys + * the default behavior is not altered + * a new alternate name provider is added (imported from `go-openapi/swag/jsonname`), aligned with JSON stdlib behavior + ## Status API is stable. @@ -108,9 +116,11 @@ on top of which it has been built. ## Limitations * [RFC6901][RFC6901] is now fully supported, including trailing "-" semantics for arrays (for `Set` operations). -* JSON name detection in go `struct`s +* Default behavior: JSON name detection in go `struct`s - Unlike go standard marshaling, untagged fields do not default to the go field name and are ignored. - anonymous fields are not traversed if untagged + - the above limitations may be overcome by calling `UseGoNameProvider()` at initialization time. + - alternatively, users may inject the desired custom behavior for naming fields as an option. ## Other documentation diff --git a/examples_test.go b/examples_test.go index 48d1755..0903be8 100644 --- a/examples_test.go +++ b/examples_test.go @@ -7,6 +7,8 @@ import ( "encoding/json" "errors" "fmt" + + "github.com/go-openapi/swag/jsonname" ) var ErrExampleStruct = errors.New("example error") @@ -185,3 +187,49 @@ func ExamplePointer_Set_appendTopLevelSlice() { // original: [1 2] // returned: [1 2 3] } + +// ExampleUseGoNameProvider contrasts the two [NameProvider] implementations +// shipped by [github.com/go-openapi/swag/jsonname]: +// +// - the default provider requires a `json` struct tag to expose a field; +// - the Go-name provider follows encoding/json conventions and accepts +// exported untagged fields and promoted embedded fields as well. +func ExampleUseGoNameProvider() { + type Embedded struct { + Nested string // untagged: promoted only by the Go-name provider + } + type Doc struct { + Embedded // untagged embedded: promoted only by the Go-name provider + + Tagged string `json:"tagged"` + Untagged string // no tag: visible only to the Go-name provider + } + + doc := Doc{ + Embedded: Embedded{Nested: "promoted"}, + Tagged: "hit", + Untagged: "hidden-by-default", + } + + for _, path := range []string{"/tagged", "/Untagged", "/Nested"} { + p, err := New(path) + if err != nil { + fmt.Println(err) + + return + } + + // Default provider: only the tagged field resolves. + defV, _, defErr := p.Get(doc) + // Go-name provider: untagged and promoted fields resolve too. + goV, _, goErr := p.Get(doc, WithNameProvider(jsonname.NewGoNameProvider())) + + fmt.Printf("%s -> default=%v (err=%v) | goname=%v (err=%v)\n", + path, defV, defErr != nil, goV, goErr != nil) + } + + // Output: + // /tagged -> default=hit (err=false) | goname=hit (err=false) + // /Untagged -> default= (err=true) | goname=hidden-by-default (err=false) + // /Nested -> default= (err=true) | goname=promoted (err=false) +} diff --git a/go.mod b/go.mod index 09d0c1e..c1a0cf0 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,7 @@ module github.com/go-openapi/jsonpointer require ( - github.com/go-openapi/swag/jsonname v0.25.5 + github.com/go-openapi/swag/jsonname v0.26.0 github.com/go-openapi/testify/v2 v2.4.2 ) diff --git a/go.sum b/go.sum index 9f4ae50..f5fc37e 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,4 @@ -github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo= -github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU= +github.com/go-openapi/swag/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w= +github.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M= github.com/go-openapi/testify/v2 v2.4.2 h1:tiByHpvE9uHrrKjOszax7ZvKB7QOgizBWGBLuq0ePx4= github.com/go-openapi/testify/v2 v2.4.2/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= diff --git a/options.go b/options.go index 0a16053..d52caab 100644 --- a/options.go +++ b/options.go @@ -23,6 +23,8 @@ var ( // SetDefaultNameProvider sets the [NameProvider] as a package-level default. // +// By default, the default provider is [jsonname.DefaultJSONNameProvider]. +// // It is safe to call concurrently with [Pointer.Get], [Pointer.Set], // [GetForToken] and [SetForToken]. The typical usage is to call it once // at initialization time. @@ -39,6 +41,20 @@ func SetDefaultNameProvider(provider NameProvider) { defaultOptions.provider = provider } +// UseGoNameProvider sets the [NameProvider] as a package-level default +// to the alternative provider [jsonname.GoNameProvider], that covers a few areas +// not supported by the default name provider. +// +// This implementation supports untagged exported fields and embedded types in go struct. +// It follows strictly the behavior of the JSON standard library regarding field naming conventions. +// +// It is safe to call concurrently with [Pointer.Get], [Pointer.Set], +// [GetForToken] and [SetForToken]. The typical usage is to call it once +// at initialization time. +func UseGoNameProvider() { + SetDefaultNameProvider(jsonname.NewGoNameProvider()) +} + // DefaultNameProvider returns the current package-level [NameProvider]. func DefaultNameProvider() NameProvider { //nolint:ireturn // returning the interface is the point — callers pick their own implementation. defaultOptionsMu.RLock() diff --git a/options_test.go b/options_test.go index e45b985..4b397fb 100644 --- a/options_test.go +++ b/options_test.go @@ -111,6 +111,28 @@ func TestSetDefaultNameProvider_nilIgnored(t *testing.T) { assert.Same(t, original, DefaultNameProvider(), "nil must be a no-op") } +func TestUseGoNameProvider_resolvesUntaggedFields(t *testing.T) { + // Not Parallel: mutates package state. + original := DefaultNameProvider() + t.Cleanup(func() { SetDefaultNameProvider(original) }) + + // optionStruct.Field has no json tag; the default provider can't resolve it, + // but the Go-name provider follows encoding/json conventions and can. + doc := optionStruct{Field: "hello"} + + p, err := New("/Field") + require.NoError(t, err) + + _, _, err = p.Get(doc) + require.Error(t, err, "default provider should not resolve untagged fields") + + UseGoNameProvider() + + v, _, err := p.Get(doc) + require.NoError(t, err) + assert.Equal(t, "hello", v) +} + func TestDefaultNameProvider_reachesGetForToken(t *testing.T) { // Not Parallel: mutates package state. original := DefaultNameProvider()