Skip to content

Commit 2196fed

Browse files
authored
fix: infer - missing nullable type for non Go types (#42)
When providing a struct containing an attribute where the type is a pointer to a non-native type, the infer analysis wouldn't set the output schema type as nullable for this attribute. Example: ```go type example struct { Value *time.Time `json:"value"` } func main() { schema, err := jsonschema.For[example](nil) if err != nil { fmt.Fprintf(os.Stderr, "failed to generate JSON schema: %v", err) os.Exit(1) } encoded, err := json.Marshal(schema) if err != nil { fmt.Fprintf(os.Stderr, "failed to encode JSON schema: %v", err) os.Exit(1) } fmt.Println(string(encoded)) } ``` This would generate: ```json {"type":"object","required":["value"],"properties":{"value":{"type":"string"}},"additionalProperties":false} ``` Using the `TypeSchemas` option will have the same result, since only the element is analysed [1]: ```go schema, err := jsonschema.For[example](&jsonschema.ForOptions{ TypeSchemas: map[reflect.Type]*jsonschema.Schema{ reflect.TypeFor[*time.Time](): { Types: []string{"null", "string"}, }, }, }) ``` With this patch the null flag will be checked even when there's a match with pre-registered types, resulting on the following output: ```json {"type":"object","required":["value"],"properties":{"value":{"type":["null","string"]}},"additionalProperties":false} ``` Resolves #41 [1] https://github.com/google/jsonschema-go/blob/3ba200528d086ef03a2d8e8d4f7d7146400e342f/jsonschema/infer.go#L114
1 parent ad34a93 commit 2196fed

File tree

2 files changed

+51
-14
lines changed

2 files changed

+51
-14
lines changed

jsonschema/infer.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,15 @@ import (
1212
"maps"
1313
"math"
1414
"math/big"
15+
"os"
1516
"reflect"
1617
"regexp"
18+
"slices"
1719
"time"
1820
)
1921

22+
const debugEnv = "JSONSCHEMAGODEBUG"
23+
2024
// ForOptions are options for the [For] and [ForType] functions.
2125
type ForOptions struct {
2226
// If IgnoreInvalidTypes is true, fields that can't be represented as a JSON
@@ -128,7 +132,16 @@ func forType(t reflect.Type, seen map[reflect.Type]bool, ignore bool, schemas ma
128132
}
129133

130134
if s := schemas[t]; s != nil {
131-
return s.CloneSchemas(), nil
135+
cloned := s.CloneSchemas()
136+
if os.Getenv(debugEnv) != "typeschemasnull=1" && allowNull {
137+
if cloned.Type != "" {
138+
cloned.Types = []string{"null", cloned.Type}
139+
cloned.Type = ""
140+
} else if !slices.Contains(cloned.Types, "null") {
141+
cloned.Types = append([]string{"null"}, cloned.Types...)
142+
}
143+
}
144+
return cloned, nil
132145
}
133146

134147
var (
@@ -332,7 +345,11 @@ func init() {
332345
ss := &Schema{Type: "string"}
333346
initialSchemaMap[reflect.TypeFor[time.Time]()] = ss
334347
initialSchemaMap[reflect.TypeFor[slog.Level]()] = ss
335-
initialSchemaMap[reflect.TypeFor[big.Int]()] = &Schema{Types: []string{"null", "string"}}
348+
if os.Getenv(debugEnv) == "typeschemasnull=1" {
349+
initialSchemaMap[reflect.TypeFor[big.Int]()] = &Schema{Types: []string{"null", "string"}}
350+
} else {
351+
initialSchemaMap[reflect.TypeFor[big.Int]()] = ss
352+
}
336353
initialSchemaMap[reflect.TypeFor[big.Rat]()] = ss
337354
initialSchemaMap[reflect.TypeFor[big.Float]()] = ss
338355
}

jsonschema/infer_test.go

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@ func TestFor(t *testing.T) {
9494
{"bool", forType[bool](ignore), &schema{Type: "boolean"}},
9595
{"time", forType[time.Time](ignore), &schema{Type: "string"}},
9696
{"level", forType[slog.Level](ignore), &schema{Type: "string"}},
97-
{"bigint", forType[big.Int](ignore), &schema{Types: []string{"null", "string"}}},
97+
{"bigint", forType[big.Int](ignore), &schema{Type: "string"}},
98+
{"bigint", forType[*big.Int](ignore), &schema{Types: []string{"null", "string"}}},
9899
{"custom", forType[custom](ignore), &schema{Type: "custom"}},
99100
{"intmap", forType[map[string]int](ignore), &schema{
100101
Type: "object",
@@ -113,7 +114,8 @@ func TestFor(t *testing.T) {
113114
forType[struct {
114115
F int `json:"f" jsonschema:"fdesc"`
115116
G []float64
116-
P *bool `jsonschema:"pdesc"`
117+
P *bool `jsonschema:"pdesc"`
118+
PT *time.Time
117119
Skip string `json:"-"`
118120
NoSkip string `json:",omitempty"`
119121
unexported float64
@@ -125,9 +127,10 @@ func TestFor(t *testing.T) {
125127
"f": {Type: "integer", Description: "fdesc"},
126128
"G": {Type: "array", Items: &schema{Type: "number"}},
127129
"P": {Types: []string{"null", "boolean"}, Description: "pdesc"},
130+
"PT": {Types: []string{"null", "string"}},
128131
"NoSkip": {Type: "string"},
129132
},
130-
Required: []string{"f", "G", "P"},
133+
Required: []string{"f", "G", "P", "PT"},
131134
AdditionalProperties: falseSchema(),
132135
},
133136
},
@@ -220,12 +223,21 @@ func TestForType(t *testing.T) {
220223
B int // hidden by S.B
221224
}
222225

226+
type M1 int
227+
type M2 int
228+
223229
type S struct {
224-
I int
225-
F func()
226-
C custom
230+
I int
231+
F func()
232+
C custom
233+
P *custom
234+
PP **custom
227235
E
228-
B bool
236+
B bool
237+
M1 M1
238+
PM1 *M1
239+
M2 M2
240+
PM2 *M2
229241
}
230242

231243
opts := &jsonschema.ForOptions{
@@ -239,6 +251,8 @@ func TestForType(t *testing.T) {
239251
"B": {Type: "integer"},
240252
},
241253
},
254+
reflect.TypeFor[M1](): {Types: []string{"custom1", "custom2"}},
255+
reflect.TypeFor[M2](): {Types: []string{"null", "custom3", "custom4"}},
242256
},
243257
}
244258
got, err := jsonschema.ForType(reflect.TypeOf(S{}), opts)
@@ -248,12 +262,18 @@ func TestForType(t *testing.T) {
248262
want := &schema{
249263
Type: "object",
250264
Properties: map[string]*schema{
251-
"I": {Type: "integer"},
252-
"C": {Type: "custom"},
253-
"G": {Type: "integer"},
254-
"B": {Type: "boolean"},
265+
"I": {Type: "integer"},
266+
"C": {Type: "custom"},
267+
"P": {Types: []string{"null", "custom"}},
268+
"PP": {Types: []string{"null", "custom"}},
269+
"G": {Type: "integer"},
270+
"B": {Type: "boolean"},
271+
"M1": {Types: []string{"custom1", "custom2"}},
272+
"PM1": {Types: []string{"null", "custom1", "custom2"}},
273+
"M2": {Types: []string{"null", "custom3", "custom4"}},
274+
"PM2": {Types: []string{"null", "custom3", "custom4"}},
255275
},
256-
Required: []string{"I", "C", "B"},
276+
Required: []string{"I", "C", "P", "PP", "B", "M1", "PM1", "M2", "PM2"},
257277
AdditionalProperties: falseSchema(),
258278
}
259279
if diff := cmp.Diff(want, got, cmpopts.IgnoreUnexported(schema{})); diff != "" {

0 commit comments

Comments
 (0)