jsonschema: support "jsonschema" struct tags (#101)

Struct fields may have the "jsonschema" struct tag, which is used as the
description of the property.

Fixes #47.
This commit is contained in:
Jonathan Amsterdam 2025-07-10 13:51:23 -04:00 • committed by GitHub
commit 2c577e564b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -5,6 +5,7 @@
package jsonschema_test
import (
"strings"
"testing"
"github.com/google/go-cmp/cmp"
@ -20,8 +21,13 @@ func forType[T any]() *jsonschema.Schema {
return s
}
func TestForType(t *testing.T) {
func TestFor(t *testing.T) {
type schema = jsonschema.Schema
type S struct {
B int `jsonschema:"bdesc"`
}
tests := []struct {
name string
got *jsonschema.Schema
@ -44,9 +50,9 @@ func TestForType(t *testing.T) {
{
"struct",
forType[struct {
F int `json:"f"`
F int `json:"f" jsonschema:"fdesc"`
G []float64
P *bool
P *bool `jsonschema:"pdesc"`
Skip string `json:"-"`
NoSkip string `json:",omitempty"`
unexported float64
@ -55,13 +61,13 @@ func TestForType(t *testing.T) {
&schema{
Type: "object",
Properties: map[string]*schema{
"f": {Type: "integer"},
"f": {Type: "integer", Description: "fdesc"},
"G": {Type: "array", Items: &schema{Type: "number"}},
"P": {Types: []string{"null", "boolean"}},
"P": {Types: []string{"null", "boolean"}, Description: "pdesc"},
"NoSkip": {Type: "string"},
},
Required: []string{"f", "G", "P"},
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}},
AdditionalProperties: falseSchema(),
},
},
{
@ -74,7 +80,37 @@ func TestForType(t *testing.T) {
"Y": {Type: "integer"},
},
Required: []string{"X", "Y"},
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}},
AdditionalProperties: falseSchema(),
},
},
{
"nested and embedded",
forType[struct {
A S
S
}](),
&schema{
Type: "object",
Properties: map[string]*schema{
"A": {
Type: "object",
Properties: map[string]*schema{
"B": {Type: "integer", Description: "bdesc"},
},
Required: []string{"B"},
AdditionalProperties: falseSchema(),
},
"S": {
Type: "object",
Properties: map[string]*schema{
"B": {Type: "integer", Description: "bdesc"},
},
Required: []string{"B"},
AdditionalProperties: falseSchema(),
},
},
Required: []string{"A", "S"},
AdditionalProperties: falseSchema(),
},
},
}
@ -92,6 +128,38 @@ func TestForType(t *testing.T) {
}
}
func forErr[T any]() error {
_, err := jsonschema.For[T]()
return err
}
func TestForErrors(t *testing.T) {
type (
s1 struct {
Empty int `jsonschema:""`
}
s2 struct {
Bad int `jsonschema:"$foo=1,bar"`
}
)
for _, tt := range []struct {
got error
want string
}{
{forErr[map[int]int](), "unsupported map key type"},
{forErr[s1](), "empty jsonschema tag"},
{forErr[s2](), "must not begin with"},
{forErr[func()](), "unsupported"},
} {
if tt.got == nil {
t.Errorf("got nil, want error containing %q", tt.want)
} else if !strings.Contains(tt.got.Error(), tt.want) {
t.Errorf("got %q\nwant it to contain %q", tt.got, tt.want)
}
}
}
func TestForWithMutation(t *testing.T) {
// This test ensures that the cached schema is not mutated when the caller
// mutates the returned schema.
@ -172,7 +240,6 @@ func TestForWithCycle(t *testing.T) {
}
for _, test := range tests {
test := test // prevent loop shadowing
t.Run(test.name, func(t *testing.T) {
err := test.fn()
if test.shouldErr && err == nil {
@ -184,3 +251,7 @@ func TestForWithCycle(t *testing.T) {
})
}
}
func falseSchema() *jsonschema.Schema {
return &jsonschema.Schema{Not: &jsonschema.Schema{}}
}