Skip to content

Commit 2d48317

Browse files
authored
Additional JSON Schema Changes (#123)
* Fix for slices * Updated examples using JSON
1 parent b53f8f5 commit 2d48317

4 files changed

Lines changed: 71 additions & 7 deletions

File tree

go.mod

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ go 1.25.0
55
require (
66
github.com/alecthomas/kong v1.14.0
77
github.com/google/jsonschema-go v0.4.2
8-
github.com/mutablelogic/go-client v1.4.3
8+
github.com/google/uuid v1.6.0
9+
github.com/mutablelogic/go-client v1.4.5
10+
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
911
github.com/stretchr/testify v1.11.1
1012
go.opentelemetry.io/otel v1.42.0
1113
go.opentelemetry.io/otel/sdk v1.42.0
@@ -20,7 +22,6 @@ require (
2022
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
2123
github.com/go-logr/logr v1.4.3 // indirect
2224
github.com/go-logr/stdr v1.2.2 // indirect
23-
github.com/google/uuid v1.6.0 // indirect
2425
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
2526
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
2627
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
@@ -29,12 +30,12 @@ require (
2930
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 // indirect
3031
go.opentelemetry.io/otel/metric v1.42.0 // indirect
3132
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
32-
golang.org/x/net v0.51.0 // indirect
33+
golang.org/x/net v0.52.0 // indirect
3334
golang.org/x/oauth2 v0.36.0 // indirect
3435
golang.org/x/sys v0.42.0 // indirect
35-
golang.org/x/text v0.34.0 // indirect
36-
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect
37-
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
36+
golang.org/x/text v0.35.0 // indirect
37+
google.golang.org/genproto/googleapis/api v0.0.0-20260330182312-d5a96adf58d8 // indirect
38+
google.golang.org/genproto/googleapis/rpc v0.0.0-20260330182312-d5a96adf58d8 // indirect
3839
google.golang.org/grpc v1.79.3 // indirect
3940
google.golang.org/protobuf v1.36.11 // indirect
4041
)

go.sum

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
3333
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
3434
github.com/mutablelogic/go-client v1.4.3 h1:bXBHc9CJrZVUnBX1roiRzx7ctIZjh85SAP4RofDsfso=
3535
github.com/mutablelogic/go-client v1.4.3/go.mod h1:cKgq1PfpeUlVirp/kL9mRvZtjVBlDjYuh9atwsLREuk=
36+
github.com/mutablelogic/go-client v1.4.5 h1:oB+ShR5qK7Wfi9su8QxN7VLiSZ8ceiMenh4eyHVjRH8=
37+
github.com/mutablelogic/go-client v1.4.5/go.mod h1:y6+N5d1IfDe4Pv/w27HVGXIQpGbcjD+1Yp7JbpvAauQ=
38+
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
39+
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
3640
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
3741
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
3842
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
@@ -63,20 +67,29 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
6367
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
6468
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
6569
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
70+
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
71+
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
6672
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
6773
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
6874
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
6975
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
76+
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
7077
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
7178
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
7279
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
7380
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
81+
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
82+
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
7483
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
7584
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
7685
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4=
7786
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng=
87+
google.golang.org/genproto/googleapis/api v0.0.0-20260330182312-d5a96adf58d8 h1:udju5p8o61FW6K2fxHWPIZhChk4FHl2Hjk8+uuLNnpM=
88+
google.golang.org/genproto/googleapis/api v0.0.0-20260330182312-d5a96adf58d8/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y=
7889
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
7990
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
91+
google.golang.org/genproto/googleapis/rpc v0.0.0-20260330182312-d5a96adf58d8 h1:OHkuo1i98/05rzpm9NBbfEtpJH/k3abEgZUKaAuCI7Y=
92+
google.golang.org/genproto/googleapis/rpc v0.0.0-20260330182312-d5a96adf58d8/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
8093
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
8194
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
8295
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=

pkg/jsonschema/jsonschema.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,17 @@ func enrichSchema(s *upstream.Schema, t reflect.Type) error {
327327
}
328328
}
329329
}
330+
331+
// Slice fields should not allow null. The upstream library adds "null"
332+
// because Go slices have a nil zero value, but in JSON APIs an absent
333+
// slice is omitted or [], never null.
334+
if ft.Kind() == reflect.Slice {
335+
prop.Types = removeString(prop.Types, "null")
336+
if prop.Type == "null" {
337+
prop.Type = ""
338+
}
339+
}
340+
330341
if v := field.Tag.Get("min"); v != "" {
331342
applyMin(prop, ft, v)
332343
}
@@ -343,7 +354,10 @@ func enrichSchema(s *upstream.Schema, t reflect.Type) error {
343354
prop.ReadOnly = true
344355
}
345356
if v := field.Tag.Get("example"); v != "" {
346-
if raw := marshalDefault(field.Type, v); raw != nil {
357+
var parsed any
358+
if err := json.Unmarshal([]byte(v), &parsed); err == nil {
359+
prop.Examples = append(prop.Examples, parsed)
360+
} else if raw := marshalDefault(field.Type, v); raw != nil {
347361
var decoded any
348362
if err := json.Unmarshal(raw, &decoded); err == nil {
349363
prop.Examples = append(prop.Examples, decoded)

pkg/jsonschema/jsonschema_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1796,3 +1796,39 @@ func TestFor_ByteSlice_StructField(t *testing.T) {
17961796
t.Errorf("data format: got %q, want \"byte\"", prop.Format)
17971797
}
17981798
}
1799+
1800+
func TestFor_SliceField_NoNull(t *testing.T) {
1801+
type S struct {
1802+
Tags []string `json:"tags"`
1803+
IDs []int `json:"ids,omitempty"`
1804+
}
1805+
s, err := For[S]()
1806+
if err != nil {
1807+
t.Fatal(err)
1808+
}
1809+
for _, name := range []string{"tags", "ids"} {
1810+
prop := s.Properties[name]
1811+
if prop == nil {
1812+
t.Fatalf("expected property %q", name)
1813+
}
1814+
// Type should be "array" with no "null" in Types.
1815+
for _, typ := range prop.Types {
1816+
if typ == "null" {
1817+
t.Errorf("%s: Types contains \"null\", want no null for slice fields", name)
1818+
}
1819+
}
1820+
if prop.Type == "null" {
1821+
t.Errorf("%s: Type is \"null\", want \"array\"", name)
1822+
}
1823+
// Ensure the schema still declares "array" as the type.
1824+
hasArray := prop.Type == "array"
1825+
for _, typ := range prop.Types {
1826+
if typ == "array" {
1827+
hasArray = true
1828+
}
1829+
}
1830+
if !hasArray {
1831+
t.Errorf("%s: schema missing \"array\" type; Type=%q, Types=%v", name, prop.Type, prop.Types)
1832+
}
1833+
}
1834+
}

0 commit comments

Comments
 (0)