Skip to content
Open
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
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ This repository is a fork of the original [jsonschema](https://github.com/alecth

- The original was stuck on the draft-04 version of JSON Schema, we've now moved to the latest JSON Schema Draft 2020-12.
- Schema IDs are added automatically from the current Go package's URL in order to be unique, and can be disabled with the `Anonymous` option.
- Support for the `FullyQualifyTypeName` option has been removed. If you have conflicts, you should use multiple schema files with different IDs, set the `DoNotReference` option to true to hide definitions completely, or add your own naming strategy using the `Namer` property.
- Support for the `FullyQualifyTypeName` option has been removed. If you have conflicts, you should use multiple schema files with different IDs, set the `DoNotReference` option to true to hide definitions completely, add your own naming strategy using the `Namer` property, or use the `AddPackageNamespaces` option described in [Type Naming and Conflicts](#type-naming-and-conflicts).
- Support for `yaml` tags and related options has been dropped for the sake of simplification. There were a [few inconsistencies](https://github.com/invopop/jsonschema/pull/21) around this that have now been fixed.

## Versions
Expand Down Expand Up @@ -246,6 +246,22 @@ Expect the results to be similar to:
}
```

### Type Naming and Conflicts

By default, definitions are named after the Go type's name (`t.Name()`). If two different packages define a type with the same name (e.g. two unrelated `Config` structs) and both end up referenced in the same schema, they will collide on the same `$defs` entry.

You have a few ways to resolve this:

- Set `Reflector.AddPackageNamespaces` to `true` to prefix every generated type name with its package name, e.g. `httpconf.Config` and `tcpconf.Config` instead of two conflicting `Config` entries.
- Provide your own `Reflector.Namer` function for full control over naming, including selectively qualifying only the types that actually conflict.
- Set `Reflector.DoNotReference` to `true` to avoid `$defs`/`$ref` altogether and inline every type in place.

```go
r := new(Reflector)
r.AddPackageNamespaces = true
schema := r.Reflect(&AllConfig{})
```

### Custom Key Naming

In some situations, the keys actually used to write files are different from Go structs'.
Expand Down
9 changes: 9 additions & 0 deletions internal/nsfixture/httpconf/httpconf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Package httpconf provides a fixture type used to test AddPackageNamespaces:
// a Config struct that collides in name with tcpconf.Config.
package httpconf

// Config holds HTTP-specific configuration.
type Config struct {
URL string `json:"url" jsonschema:"required,format=uri"`
Method string `json:"method" jsonschema:"required"`
}
9 changes: 9 additions & 0 deletions internal/nsfixture/tcpconf/tcpconf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Package tcpconf provides a fixture type used to test AddPackageNamespaces:
// a Config struct that collides in name with httpconf.Config.
package tcpconf

// Config holds TCP-specific configuration.
type Config struct {
Host string `json:"host" jsonschema:"required,format=hostname"`
Port int `json:"port" jsonschema:"required,minimum=1,maximum=65535"`
}
16 changes: 15 additions & 1 deletion reflect.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"encoding/json"
"net"
"net/url"
"path"
"reflect"
"strconv"
"strings"
Expand Down Expand Up @@ -135,6 +136,13 @@ type Reflector struct {
// provided by the reflect package.
Namer func(reflect.Type) string

// AddPackageNamespaces will prefix the generated name of a type with its package
// name (the last element of its import path) when set to true, e.g. "http.Config"
// instead of "Config". This helps disambiguate identically-named types declared in
// different packages, which would otherwise collide on the same $ref/$defs entry.
// It has no effect on types for which Namer returns a non-empty name.
AddPackageNamespaces bool

// KeyNamer allows customizing of key names.
// The default is to use the key's name as is, or the json tag if present.
// If a json tag is present, KeyNamer will receive the tag's name as an argument, not the original key name.
Expand Down Expand Up @@ -1145,7 +1153,13 @@ func (r *Reflector) typeName(t reflect.Type) string {
return name
}
}
return t.Name()
name := t.Name()
if r.AddPackageNamespaces {
if pkg := path.Base(t.PkgPath()); pkg != "" && pkg != "." {
name = pkg + "." + name
}
}
return name
}

// Split on commas that are not preceded by `\`.
Expand Down
37 changes: 37 additions & 0 deletions reflect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import (

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/invopop/jsonschema/internal/nsfixture/httpconf"
"github.com/invopop/jsonschema/internal/nsfixture/tcpconf"
)

var updateFixtures = flag.Bool("update", false, "set to update fixtures")
Expand Down Expand Up @@ -366,6 +369,40 @@ func TestReflectFromType(t *testing.T) {
assert.Empty(t, s.ID)
}

func TestAddPackageNamespacesDisambiguatesSameNameStructs(t *testing.T) {
type AllConfig struct {
TCP tcpconf.Config `json:"tcp"`
HTTP httpconf.Config `json:"http"`
}

r := &Reflector{AddPackageNamespaces: true}
s := r.Reflect(&AllConfig{})

tcpRef, ok := s.Definitions["jsonschema.AllConfig"].Properties.Get("tcp")
require.True(t, ok)
httpRef, ok := s.Definitions["jsonschema.AllConfig"].Properties.Get("http")
require.True(t, ok)

assert.Equal(t, "#/$defs/tcpconf.Config", tcpRef.Ref)
assert.Equal(t, "#/$defs/httpconf.Config", httpRef.Ref)
assert.NotEqual(t, tcpRef.Ref, httpRef.Ref)

_, hasTCPConfig := s.Definitions["tcpconf.Config"]
_, hasHTTPConfig := s.Definitions["httpconf.Config"]
assert.True(t, hasTCPConfig)
assert.True(t, hasHTTPConfig)

// Without AddPackageNamespaces, both structs collide on the same "Config" $ref.
rDefault := new(Reflector)
sDefault := rDefault.Reflect(&AllConfig{})
tcpRefDefault, ok := sDefault.Definitions["AllConfig"].Properties.Get("tcp")
require.True(t, ok)
httpRefDefault, ok := sDefault.Definitions["AllConfig"].Properties.Get("http")
require.True(t, ok)
assert.Equal(t, "#/$defs/Config", tcpRefDefault.Ref)
assert.Equal(t, "#/$defs/Config", httpRefDefault.Ref)
}

func TestSchemaGeneration(t *testing.T) {
tests := []struct {
typ any
Expand Down