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
14 changes: 14 additions & 0 deletions jsonschema/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,20 @@ implementations. See [learnjsonschema.com] for more recommendations about "forma
The content keywords described in [section 8 of the validation spec]
are recorded in the schema, but ignored during validation.

# Controlling behavior changes

Minor and patch releases of this package may introduce behavior changes as part
of bug fixes or correctness improvements. To help manage the impact of such
changes, the package allows you to access previous behaviors using the
`JSONSCHEMAGODEBUG` environment variable. The available settings are listed
below; additional options may be introduced in future releases.

- **typeschemasnull**: When set to `"1"`, the inferred schema for slices will
*not* include the `null` type alongside the array type. It will also avoid
adding `null` to non-native pointer types (such as `time.Time`). This restores
the behavior from versions prior to v0.3.0. The default behavior is to include
`null` in these cases.

[JSON Schema specification]: https://json-schema.org
[section 7 of the validation spec]: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.7
[section 8 of the validation spec]: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.8
Expand Down
6 changes: 5 additions & 1 deletion jsonschema/infer.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,11 @@ func forType(t reflect.Type, seen map[reflect.Type]bool, ignore bool, schemas ma
}

case reflect.Slice, reflect.Array:
s.Type = "array"
if os.Getenv(debugEnv) != "typeschemasnull=1" && t.Kind() == reflect.Slice {
s.Types = []string{"null", "array"}
} else {
s.Type = "array"
}
itemsSchema, err := forType(t.Elem(), seen, ignore, schemas)
if err != nil {
return nil, fmt.Errorf("computing element schema: %v", err)
Expand Down
4 changes: 3 additions & 1 deletion jsonschema/infer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ func TestFor(t *testing.T) {
{"level", forType[slog.Level](ignore), &schema{Type: "string"}},
{"bigint", forType[big.Int](ignore), &schema{Type: "string"}},
{"bigint", forType[*big.Int](ignore), &schema{Types: []string{"null", "string"}}},
{"int64slice", forType[[]int64](ignore), &schema{Types: []string{"null", "array"}, Items: &schema{Type: "integer"}}},
{"int64array", forType[[2]int64](ignore), &schema{Type: "array", Items: &schema{Type: "integer"}, MinItems: jsonschema.Ptr(2), MaxItems: jsonschema.Ptr(2)}},
{"custom", forType[custom](ignore), &schema{Type: "custom"}},
{"intmap", forType[map[string]int](ignore), &schema{
Type: "object",
Expand Down Expand Up @@ -125,7 +127,7 @@ func TestFor(t *testing.T) {
Type: "object",
Properties: map[string]*schema{
"f": {Type: "integer", Description: "fdesc"},
"G": {Type: "array", Items: &schema{Type: "number"}},
"G": {Types: []string{"null", "array"}, Items: &schema{Type: "number"}},
"P": {Types: []string{"null", "boolean"}, Description: "pdesc"},
"PT": {Types: []string{"null", "string"}},
"NoSkip": {Type: "string"},
Expand Down
32 changes: 27 additions & 5 deletions jsonschema/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -509,10 +509,10 @@ func TestStructEmbedding(t *testing.T) {
validInstance any
}{
{
name: "ExportedPointer",
name: "Slice",
targetType: reflect.TypeOf([]Banana{}),
wantSchema: &Schema{
Type: "array",
Types: []string{"null", "array"},
Items: &Schema{
Type: "object",
Properties: map[string]*Schema{
Expand All @@ -528,11 +528,33 @@ func TestStructEmbedding(t *testing.T) {
{Apple: &Apple{ID: "foo2", Name: "Test Foo 2"}, Extra: "additional data 2"},
},
},
{
name: "Array",
targetType: reflect.TypeOf([2]Banana{}),
wantSchema: &Schema{
Type: "array",
MinItems: Ptr(2),
MaxItems: Ptr(2),
Items: &Schema{
Type: "object",
Properties: map[string]*Schema{
"id": {Type: "string"},
"name": {Type: "string"},
"extra": {Type: "string"},
},
Required: []string{"id", "name", "extra"},
AdditionalProperties: falseSchema(),
}},
validInstance: [2]Banana{
{Apple: &Apple{ID: "foo1", Name: "Test Foo 2"}, Extra: "additional data 1"},
{Apple: &Apple{ID: "foo2", Name: "Test Foo 2"}, Extra: "additional data 2"},
},
},
{
name: "UnExportedPointer",
targetType: reflect.TypeOf([]Durian{}),
wantSchema: &Schema{
Type: "array",
Types: []string{"null", "array"},
Items: &Schema{
Type: "object",
Properties: map[string]*Schema{
Expand All @@ -552,7 +574,7 @@ func TestStructEmbedding(t *testing.T) {
name: "ExportedValue",
targetType: reflect.TypeOf([]Fig{}),
wantSchema: &Schema{
Type: "array",
Types: []string{"null", "array"},
Items: &Schema{
Type: "object",
Properties: map[string]*Schema{
Expand All @@ -572,7 +594,7 @@ func TestStructEmbedding(t *testing.T) {
name: "UnExportedValue",
targetType: reflect.TypeOf([]Honeyberry{}),
wantSchema: &Schema{
Type: "array",
Types: []string{"null", "array"},
Items: &Schema{
Type: "object",
Properties: map[string]*Schema{
Expand Down