11package jsonschema_test
22
33import (
4+ "encoding/json"
45 "fmt"
56 "go/ast"
67 "go/parser"
@@ -234,10 +235,28 @@ func TestGenerateGo_typeMatrix(t *testing.T) {
234235 {"const_string" , `{"const":"x"}` , "string" },
235236 {"const_bool" , `{"const":true}` , "bool" },
236237
237- // Type unions.
238- {"nullable_string" , `{"type":["string","null"]}` , "string" },
238+ // Type unions. A scalar + null union is the presence-carrying mapping:
239+ // the pointer distinguishes "absent (or null)" from the zero value —
240+ // the contract keywords like a spec's `minimum` rely on (0 declared
241+ // vs no bound at all). Containers stay bare: nil is already absence.
242+ {"nullable_string" , `{"type":["string","null"]}` , "*string" },
243+ {"nullable_integer" , `{"type":["integer","null"]}` , "*int" },
244+ {"nullable_number" , `{"type":["number","null"]}` , "*float64" },
245+ {"nullable_boolean" , `{"type":["boolean","null"]}` , "*bool" },
246+ {"nullable_array" , `{"type":["array","null"],"items":{"type":"string"}}` , "[]string" },
247+ {"nullable_items" , `{"type":"array","items":{"type":["integer","null"]}}` , "[]*int" },
248+ {"nullable_map_value" , `{"type":"object","additionalProperties":{"type":["string","null"]}}` , "map[string]*string" },
239249 {"union_multi" , `{"type":["string","integer"]}` , "any" },
240250
251+ // The anyOf nullable form — what WithGenerateNullablePointers emits
252+ // for a Go pointer — round-trips back to the pointer, either order.
253+ {"anyof_null_number" , `{"anyOf":[{"type":"null"},{"type":"number"}]}` , "*float64" },
254+ {"anyof_number_null" , `{"anyOf":[{"type":"integer"},{"type":"null"}]}` , "*int" },
255+ {"anyof_null_ref" , `{"anyOf":[{"type":"null"},{"$ref":"#/definitions/Ref"}]}` , "*Ref" },
256+ {"anyof_null_array" , `{"anyOf":[{"type":"null"},{"type":"array","items":{"type":"string"}}]}` , "*[]string" },
257+ {"anyof_null_any" , `{"anyOf":[{"type":"null"},{}]}` , "any" },
258+ {"anyof_three_way" , `{"anyOf":[{"type":"null"},{"type":"string"},{"type":"integer"}]}` , "any" },
259+
241260 // Constructs with no faithful Go mapping: degrade to any.
242261 {"oneOf" , `{"oneOf":[{"type":"string"},{"type":"integer"}]}` , "any" },
243262 {"anyOf" , `{"anyOf":[{"type":"string"},{"type":"boolean"}]}` , "any" },
@@ -254,6 +273,61 @@ func TestGenerateGo_typeMatrix(t *testing.T) {
254273 }
255274}
256275
276+ // TestGenerateGo_nullableScalars pins the presence-carrying contract around
277+ // requiredness: a nullable scalar is a pointer whether the field is required
278+ // or not (null is a legal VALUE — requiredness governs the json tag, never
279+ // strips the pointer), and the optionality rule for objects cannot
280+ // double-pointer it.
281+ func TestGenerateGo_nullableScalars (t * testing.T ) {
282+ schema := []byte (`{
283+ "type": "object",
284+ "required": ["min"],
285+ "properties": {
286+ "min": {"type": ["number", "null"]},
287+ "max": {"type": ["number", "null"]}
288+ }
289+ }` )
290+ src , err := jsonschema .GenerateGo (schema , jsonschema .WithGoRootType ("Root" ))
291+ if err != nil {
292+ t .Fatalf ("GenerateGo: %v" , err )
293+ }
294+ if got := structFieldType (t , src , "Root" , "Min" ); got != "*float64" {
295+ t .Errorf ("required nullable scalar = %q, want *float64" , got )
296+ }
297+ if got := structFieldType (t , src , "Root" , "Max" ); got != "*float64" {
298+ t .Errorf ("optional nullable scalar = %q, want *float64" , got )
299+ }
300+ }
301+
302+ // TestGenerateGo_nullableRoundTrip pins the two directions against each
303+ // other: a Go pointer field, emitted as a schema under
304+ // WithGenerateNullablePointers (the anyOf:[null, T] shape), regenerates the
305+ // same pointer field through GenerateGo.
306+ func TestGenerateGo_nullableRoundTrip (t * testing.T ) {
307+ type Bounds struct {
308+ Minimum * float64 `json:"minimum"`
309+ Label string `json:"label"`
310+ }
311+ schema , err := jsonschema .Generate (Bounds {}, jsonschema .WithGenerateNullablePointers (true ))
312+ if err != nil {
313+ t .Fatalf ("Generate: %v" , err )
314+ }
315+ raw , err := json .Marshal (schema )
316+ if err != nil {
317+ t .Fatalf ("marshal schema: %v" , err )
318+ }
319+ src , err := jsonschema .GenerateGo (raw , jsonschema .WithGoRootType ("Bounds" ))
320+ if err != nil {
321+ t .Fatalf ("GenerateGo: %v" , err )
322+ }
323+ if got := structFieldType (t , src , "Bounds" , "Minimum" ); got != "*float64" {
324+ t .Errorf ("round-tripped pointer = %q, want *float64" , got )
325+ }
326+ if got := structFieldType (t , src , "Bounds" , "Label" ); got != "string" {
327+ t .Errorf ("round-tripped value = %q, want string" , got )
328+ }
329+ }
330+
257331// TestGenerateGo_requiredFields verifies that required scalars are value
258332// types without omitempty and that a required object reference is a value
259333// (not a pointer).
0 commit comments