Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down
48 changes: 48 additions & 0 deletions examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"encoding/json"
"errors"
"fmt"

"github.com/go-openapi/swag/jsonname"
)

var ErrExampleStruct = errors.New("example error")
Expand Down Expand Up @@ -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=<nil> (err=true) | goname=hidden-by-default (err=false)
// /Nested -> default=<nil> (err=true) | goname=promoted (err=false)
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -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
)

Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
16 changes: 16 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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()
Expand Down
22 changes: 22 additions & 0 deletions options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading