Skip to content

Commit 331ea42

Browse files
authored
feat: added alternate json name provider (#123)
Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
1 parent 04588aa commit 331ea42

6 files changed

Lines changed: 100 additions & 4 deletions

File tree

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ Or join our Slack channel: [![Slack Channel][slack-logo]![slack-badge]][slack-ur
3232
trailing "-", either pass a mutable array (`*[]T`) as the input document, or use the returned updated document instead.
3333
* types that implement the `JSONSetable` interface may not implement the mutation implied by the trailing "-"
3434

35+
* **2026-04-15** : added support for optional alternate JSON name providers
36+
* for struct support the defaults might not suit all situations: there are known limitations
37+
when it comes to handle untagged fields or embedded types.
38+
* the default name provider in use is not fully aligned with go JSON stdlib
39+
* exposed an option (or global setting) to change the provider that resolves a struct into json keys
40+
* the default behavior is not altered
41+
* a new alternate name provider is added (imported from `go-openapi/swag/jsonname`), aligned with JSON stdlib behavior
42+
3543
## Status
3644

3745
API is stable.
@@ -108,9 +116,11 @@ on top of which it has been built.
108116
## Limitations
109117

110118
* [RFC6901][RFC6901] is now fully supported, including trailing "-" semantics for arrays (for `Set` operations).
111-
* JSON name detection in go `struct`s
119+
* Default behavior: JSON name detection in go `struct`s
112120
- Unlike go standard marshaling, untagged fields do not default to the go field name and are ignored.
113121
- anonymous fields are not traversed if untagged
122+
- the above limitations may be overcome by calling `UseGoNameProvider()` at initialization time.
123+
- alternatively, users may inject the desired custom behavior for naming fields as an option.
114124

115125
## Other documentation
116126

examples_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"encoding/json"
88
"errors"
99
"fmt"
10+
11+
"github.com/go-openapi/swag/jsonname"
1012
)
1113

1214
var ErrExampleStruct = errors.New("example error")
@@ -185,3 +187,49 @@ func ExamplePointer_Set_appendTopLevelSlice() {
185187
// original: [1 2]
186188
// returned: [1 2 3]
187189
}
190+
191+
// ExampleUseGoNameProvider contrasts the two [NameProvider] implementations
192+
// shipped by [github.com/go-openapi/swag/jsonname]:
193+
//
194+
// - the default provider requires a `json` struct tag to expose a field;
195+
// - the Go-name provider follows encoding/json conventions and accepts
196+
// exported untagged fields and promoted embedded fields as well.
197+
func ExampleUseGoNameProvider() {
198+
type Embedded struct {
199+
Nested string // untagged: promoted only by the Go-name provider
200+
}
201+
type Doc struct {
202+
Embedded // untagged embedded: promoted only by the Go-name provider
203+
204+
Tagged string `json:"tagged"`
205+
Untagged string // no tag: visible only to the Go-name provider
206+
}
207+
208+
doc := Doc{
209+
Embedded: Embedded{Nested: "promoted"},
210+
Tagged: "hit",
211+
Untagged: "hidden-by-default",
212+
}
213+
214+
for _, path := range []string{"/tagged", "/Untagged", "/Nested"} {
215+
p, err := New(path)
216+
if err != nil {
217+
fmt.Println(err)
218+
219+
return
220+
}
221+
222+
// Default provider: only the tagged field resolves.
223+
defV, _, defErr := p.Get(doc)
224+
// Go-name provider: untagged and promoted fields resolve too.
225+
goV, _, goErr := p.Get(doc, WithNameProvider(jsonname.NewGoNameProvider()))
226+
227+
fmt.Printf("%s -> default=%v (err=%v) | goname=%v (err=%v)\n",
228+
path, defV, defErr != nil, goV, goErr != nil)
229+
}
230+
231+
// Output:
232+
// /tagged -> default=hit (err=false) | goname=hit (err=false)
233+
// /Untagged -> default=<nil> (err=true) | goname=hidden-by-default (err=false)
234+
// /Nested -> default=<nil> (err=true) | goname=promoted (err=false)
235+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module github.com/go-openapi/jsonpointer
22

33
require (
4-
github.com/go-openapi/swag/jsonname v0.25.5
4+
github.com/go-openapi/swag/jsonname v0.26.0
55
github.com/go-openapi/testify/v2 v2.4.2
66
)
77

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo=
2-
github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU=
1+
github.com/go-openapi/swag/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w=
2+
github.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M=
33
github.com/go-openapi/testify/v2 v2.4.2 h1:tiByHpvE9uHrrKjOszax7ZvKB7QOgizBWGBLuq0ePx4=
44
github.com/go-openapi/testify/v2 v2.4.2/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=

options.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ var (
2323

2424
// SetDefaultNameProvider sets the [NameProvider] as a package-level default.
2525
//
26+
// By default, the default provider is [jsonname.DefaultJSONNameProvider].
27+
//
2628
// It is safe to call concurrently with [Pointer.Get], [Pointer.Set],
2729
// [GetForToken] and [SetForToken]. The typical usage is to call it once
2830
// at initialization time.
@@ -39,6 +41,20 @@ func SetDefaultNameProvider(provider NameProvider) {
3941
defaultOptions.provider = provider
4042
}
4143

44+
// UseGoNameProvider sets the [NameProvider] as a package-level default
45+
// to the alternative provider [jsonname.GoNameProvider], that covers a few areas
46+
// not supported by the default name provider.
47+
//
48+
// This implementation supports untagged exported fields and embedded types in go struct.
49+
// It follows strictly the behavior of the JSON standard library regarding field naming conventions.
50+
//
51+
// It is safe to call concurrently with [Pointer.Get], [Pointer.Set],
52+
// [GetForToken] and [SetForToken]. The typical usage is to call it once
53+
// at initialization time.
54+
func UseGoNameProvider() {
55+
SetDefaultNameProvider(jsonname.NewGoNameProvider())
56+
}
57+
4258
// DefaultNameProvider returns the current package-level [NameProvider].
4359
func DefaultNameProvider() NameProvider { //nolint:ireturn // returning the interface is the point — callers pick their own implementation.
4460
defaultOptionsMu.RLock()

options_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,28 @@ func TestSetDefaultNameProvider_nilIgnored(t *testing.T) {
111111
assert.Same(t, original, DefaultNameProvider(), "nil must be a no-op")
112112
}
113113

114+
func TestUseGoNameProvider_resolvesUntaggedFields(t *testing.T) {
115+
// Not Parallel: mutates package state.
116+
original := DefaultNameProvider()
117+
t.Cleanup(func() { SetDefaultNameProvider(original) })
118+
119+
// optionStruct.Field has no json tag; the default provider can't resolve it,
120+
// but the Go-name provider follows encoding/json conventions and can.
121+
doc := optionStruct{Field: "hello"}
122+
123+
p, err := New("/Field")
124+
require.NoError(t, err)
125+
126+
_, _, err = p.Get(doc)
127+
require.Error(t, err, "default provider should not resolve untagged fields")
128+
129+
UseGoNameProvider()
130+
131+
v, _, err := p.Get(doc)
132+
require.NoError(t, err)
133+
assert.Equal(t, "hello", v)
134+
}
135+
114136
func TestDefaultNameProvider_reachesGetForToken(t *testing.T) {
115137
// Not Parallel: mutates package state.
116138
original := DefaultNameProvider()

0 commit comments

Comments
 (0)