go-sdk/internal/jsonschema/schema_test.go
Jonathan Amsterdam 40185dd013 jsonschema: represent integer keywords as ints
Some JSON Schema keywords, like minLength, are logically integers.
The JSON Schema spec allows them to be written as floating-point
values with zero fractional part, which is a reasonable choice, since
JSON doesn't distinguish the two.

We could simply validate these fields as integers in the normal course
of events (namely, validating the schema against its meta-schema),
but this CL does a little better: it uses *int struct fields for
these keywords, preventing them from ever being non-integers.
The UnmarshalJSON method still accepts integral floating-point values.
Marshaling is unaffected, and the results don't change: encoding/json
has always marshaled integral floats without the decimal part.

Other choices:
- Leave them as float64: you could then get away with
    Schema{MinLength: Ptr(1.5)}
  and though you couldn't get far with that, it's still irksome.

- json.Number: same problem as float64, plus it's even harder to write
  as a literal.

- int64: now `Ptr(1)` doesn't work (it's a *int, not a *int64).
  Anyway, these keywords all compare against len(x) in Go, which
  is of type int.

Change-Id: Ie3f08bd8a5deaf2a21344d6ab873551c011b12d5
Reviewed-on: https://go-review.googlesource.com/c/tools/+/668315
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Robert Findley <rfindley@google.com>
2025-05-02 10:55:18 -07:00

108 lines
3.3 KiB
Go

// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package jsonschema
import (
"encoding/json"
"regexp"
"testing"
)
func TestGoRoundTrip(t *testing.T) {
// Verify that Go representations round-trip.
for _, s := range []*Schema{
{Type: "null"},
{Types: []string{"null", "number"}},
{Type: "string", MinLength: Ptr(20)},
{Minimum: Ptr(20.0)},
{Items: &Schema{Type: "integer"}},
{Const: Ptr(any(0))},
{Const: Ptr(any(nil))},
{Const: Ptr(any([]int{}))},
{Const: Ptr(any(map[string]any{}))},
} {
data, err := json.Marshal(s)
if err != nil {
t.Fatal(err)
}
t.Logf("marshal: %s", data)
var got *Schema
if err := json.Unmarshal(data, &got); err != nil {
t.Fatal(err)
}
if !Equal(got, s) {
t.Errorf("got %+v, want %+v", got, s)
if got.Const != nil && s.Const != nil {
t.Logf("Consts: got %#v (%[1]T), want %#v (%[2]T)", *got.Const, *s.Const)
}
}
}
}
func TestJSONRoundTrip(t *testing.T) {
// Verify that JSON texts for schemas marshal into equivalent forms.
// We don't expect everything to round-trip perfectly. For example, "true" and "false"
// will turn into their object equivalents.
// But most things should.
// Some of these cases test Schema.{UnM,M}arshalJSON.
// Most of others follow from the behavior of encoding/json, but they are still
// valuable as regression tests of this package's behavior.
for _, tt := range []struct {
in, want string
}{
{`true`, `{}`}, // boolean schemas become object schemas
{`false`, `{"not":{}}`},
{`{"type":"", "enum":null}`, `{}`}, // empty fields are omitted
{`{"minimum":1}`, `{"minimum":1}`},
{`{"minimum":1.0}`, `{"minimum":1}`}, // floating-point integers lose their fractional part
{`{"minLength":1.0}`, `{"minLength":1}`}, // some floats are unmarshaled into ints, but you can't tell
{
// map keys are sorted
`{"$vocabulary":{"b":true, "a":false}}`,
`{"$vocabulary":{"a":false,"b":true}}`,
},
{`{"unk":0}`, `{}`}, // unknown fields are dropped, unfortunately
} {
var s Schema
if err := json.Unmarshal([]byte(tt.in), &s); err != nil {
t.Fatal(err)
}
data, err := json.Marshal(s)
if err != nil {
t.Fatal(err)
}
if got := string(data); got != tt.want {
t.Errorf("%s:\ngot %s\nwant %s", tt.in, got, tt.want)
}
}
}
func TestUnmarshalErrors(t *testing.T) {
for _, tt := range []struct {
in string
want string // error must match this regexp
}{
{`1`, "cannot unmarshal number"},
{`{"type":1}`, `invalid value for "type"`},
{`{"minLength":1.5}`, `minLength:.*not an int`},
{`{"maxLength":1.5}`, `maxLength:.*not an int`},
{`{"minItems":1.5}`, `minItems:.*not an int`},
{`{"maxItems":1.5}`, `maxItems:.*not an int`},
{`{"minProperties":1.5}`, `minProperties:.*not an int`},
{`{"maxProperties":1.5}`, `maxProperties:.*not an int`},
{`{"minContains":1.5}`, `minContains:.*not an int`},
{`{"maxContains":1.5}`, `maxContains:.*not an int`},
} {
var s Schema
err := json.Unmarshal([]byte(tt.in), &s)
if err == nil {
t.Fatalf("%s: no error but expected one", tt.in)
}
if !regexp.MustCompile(tt.want).MatchString(err.Error()) {
t.Errorf("%s: error %q does not match %q", tt.in, err, tt.want)
}
}
}