// Copyright 2025 The JSON Schema Go Project 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 jsonschemaimport ()// Equal reports whether two Go values representing JSON values are equal according// to the JSON Schema spec.// The values must not contain cycles.// See https://json-schema.org/draft/2020-12/json-schema-core#section-4.2.2.// It behaves like reflect.DeepEqual, except that numbers are compared according// to mathematical equality.func (, any) bool {returnequalValue(reflect.ValueOf(), reflect.ValueOf())}func equalValue(, reflect.Value) bool {// Copied from src/reflect/deepequal.go, omitting the visited check (because JSON // values are trees).if !.IsValid() || !.IsValid() {return .IsValid() == .IsValid() }// Treat numbers specially. , := jsonNumber() , := jsonNumber()if && {return .Cmp() == 0 }if .Kind() != .Kind() {returnfalse }switch .Kind() {casereflect.Array:if .Len() != .Len() {returnfalse }for := range .Len() {if !(.Index(), .Index()) {returnfalse } }returntruecasereflect.Slice:if .IsNil() != .IsNil() {returnfalse }if .Len() != .Len() {returnfalse }if .UnsafePointer() == .UnsafePointer() {returntrue }// Special case for []byte, which is common.if .Type().Elem().Kind() == reflect.Uint8 && .Type() == .Type() {returnbytes.Equal(.Bytes(), .Bytes()) }for := range .Len() {if !(.Index(), .Index()) {returnfalse } }returntruecasereflect.Interface:if .IsNil() || .IsNil() {return .IsNil() == .IsNil() }return (.Elem(), .Elem())casereflect.Pointer:if .UnsafePointer() == .UnsafePointer() {returntrue }return (.Elem(), .Elem())casereflect.Struct: := .Type()if != .Type() {returnfalse }for := range .NumField() { := .Field()if !.IsExported() {continue }if !(.FieldByIndex(.Index), .FieldByIndex(.Index)) {returnfalse } }returntruecasereflect.Map:if .IsNil() != .IsNil() {returnfalse }if .Len() != .Len() {returnfalse }if .UnsafePointer() == .UnsafePointer() {returntrue } := .MapRange()for .Next() { := .Value() := .MapIndex(.Key())if !.IsValid() || !(, ) {returnfalse } }returntruecasereflect.Func:if .Type() != .Type() {returnfalse }if .IsNil() && .IsNil() {returntrue }panic("cannot compare functions")casereflect.String:return .String() == .String()casereflect.Bool:return .Bool() == .Bool()// Ints, uints and floats handled in jsonNumber, at top of function.default:panic(fmt.Sprintf("unsupported kind: %s", .Kind())) }}// hashValue adds v to the data hashed by h. v must not have cycles.// hashValue panics if the value contains functions or channels, or maps whose// key type is not string.// It ignores unexported fields of structs.// Calls to hashValue with the equal values (in the sense// of [Equal]) result in the same sequence of values written to the hash.func hashValue( *maphash.Hash, reflect.Value) {// TODO: replace writes of basic types with WriteComparable in 1.24. := func( uint64) {var [8]bytebinary.BigEndian.PutUint64([:], ) .Write([:]) }varfunc(reflect.Value) = func( reflect.Value) {if , := jsonNumber(); {// We want 1.0 and 1 to hash the same. // big.Rats are always normalized, so they will be. // We could do this more efficiently by handling the int and float cases // separately, but that's premature. (uint64(.Sign() + 1)) .Write(.Num().Bytes()) .Write(.Denom().Bytes())return }switch .Kind() {casereflect.Invalid: .WriteByte(0)casereflect.String: .WriteString(.String())casereflect.Bool:if .Bool() { .WriteByte(1) } else { .WriteByte(0) }casereflect.Complex64, reflect.Complex128: := .Complex() (math.Float64bits(real())) (math.Float64bits(imag()))casereflect.Array, reflect.Slice:// Although we could treat []byte more efficiently, // JSON values are unlikely to contain them. (uint64(.Len()))for := range .Len() { (.Index()) }casereflect.Interface, reflect.Pointer: (.Elem())casereflect.Struct: := .Type()for := range .NumField() {if := .Field(); .IsExported() { (.FieldByIndex(.Index)) } }casereflect.Map:if .Type().Key().Kind() != reflect.String {panic("map with non-string key") }// Sort the keys so the hash is deterministic. := .MapKeys()// Write the length. That distinguishes between, say, two consecutive // maps with disjoint keys from one map that has the items of both. (uint64(len()))slices.SortFunc(, func(, reflect.Value) int { returncmp.Compare(.String(), .String()) })for , := range { () (.MapIndex()) }// Ints, uints and floats handled in jsonNumber, at top of function.default:panic(fmt.Sprintf("unsupported kind: %s", .Kind())) } } ()}// jsonNumber converts a numeric value or a json.Number to a [big.Rat].// If v is not a number, it returns nil, false.func jsonNumber( reflect.Value) (*big.Rat, bool) { := new(big.Rat)switch {case !.IsValid():returnnil, falsecase .CanInt(): .SetInt64(.Int())case .CanUint(): .SetUint64(.Uint())case .CanFloat(): .SetFloat64(.Float())default: , := .Interface().(json.Number)if ! {returnnil, false }if , := .SetString(.String()); ! {// This can fail in rare cases; for example, "1e9999999". // That is a valid JSON number, since the spec puts no limit on the size // of the exponent.returnnil, false } }return , true}// jsonType returns a string describing the type of the JSON value,// as described in the JSON Schema specification:// https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.1.1.// It returns "", false if the value is not valid JSON.func jsonType( reflect.Value) (string, bool) {if !.IsValid() {// Not v.IsNil(): a nil []any is still a JSON array.return"null", true }if .CanInt() || .CanUint() {return"integer", true }if .CanFloat() {if , := math.Modf(.Float()); == 0 {return"integer", true }return"number", true }switch .Kind() {casereflect.Bool:return"boolean", truecasereflect.String:return"string", truecasereflect.Slice, reflect.Array:return"array", truecasereflect.Map, reflect.Struct:return"object", truedefault:return"", false }}func assert( bool, string) {if ! {panic("assertion failed: " + ) }}// marshalStructWithMap marshals its first argument to JSON, treating the field named// mapField as an embedded map. The first argument must be a pointer to// a struct. The underlying type of mapField must be a map[string]any, and it must have// a "-" json tag, meaning it will not be marshaled.//// For example, given this struct://// type S struct {// A int// Extra map[string] any `json:"-"`// }//// and this value://// s := S{A: 1, Extra: map[string]any{"B": 2}}//// the call marshalJSONWithMap(s, "Extra") would return//// {"A": 1, "B": 2}//// It is an error if the map contains the same key as another struct field's// JSON name.//// marshalStructWithMap calls json.Marshal on a value of type T, so T must not// have a MarshalJSON method that calls this function, on pain of infinite regress.//// Note that there is a similar function in mcp/util.go, but they are not the same.// Here the function requires `-` json tag, does not clear the mapField map,// and handles embedded struct due to the implementation of jsonNames in this package.//// TODO: avoid this restriction on T by forcing it to marshal in a default way.// See https://go.dev/play/p/EgXKJHxEx_R.func marshalStructWithMap[ any]( *, string) ([]byte, error) {// Marshal the struct and the map separately, and concatenate the bytes. // This strategy is dramatically less complicated than // constructing a synthetic struct or map with the combined keys.if == nil {return []byte("null"), nil } := * := reflect.ValueOf(&).Elem().FieldByName() := .Interface().(map[string]any)// Check for duplicates. := jsonNames(reflect.TypeFor[]())for := range {if [] {returnnil, fmt.Errorf("map key %q duplicates struct field", ) } } , := json.Marshal()if != nil {returnnil, fmt.Errorf("marshalStructWithMap(%+v): %w", , ) }iflen() == 0 {return , nil } , := json.Marshal()if != nil {returnnil, }iflen() == 2 { // must be "{}"return , nil }// "{X}" + "{Y}" => "{X,Y}" := append([:len()-1], ',') = append(, [1:]...)return , nil}// unmarshalStructWithMap is the inverse of marshalStructWithMap.// T has the same restrictions as in that function.//// Note that there is a similar function in mcp/util.go, but they are not the same.// Here jsonNames also returns fields from embedded structs, hence this function// handles embedded structs as well.func unmarshalStructWithMap[ any]( []byte, *, string) error {// Unmarshal into the struct, ignoring unknown fields.if := json.Unmarshal(, ); != nil {return }// Unmarshal into the map. := map[string]any{}if := json.Unmarshal(, &); != nil {return }// Delete from the map the fields of the struct.for := rangejsonNames(reflect.TypeFor[]()) {delete(, ) }iflen() != 0 {reflect.ValueOf().Elem().FieldByName().Set(reflect.ValueOf()) }returnnil}var jsonNamesMap sync.Map// from reflect.Type to map[string]bool// jsonNames returns the set of JSON object keys that t will marshal into,// including fields from embedded structs in t.// t must be a struct type.//// Note that there is a similar function in mcp/util.go, but they are not the same// Here the function recurses over embedded structs and includes fields from them.func jsonNames( reflect.Type) map[string]bool {// Lock not necessary: at worst we'll duplicate work.if , := jsonNamesMap.Load(); {return .(map[string]bool) } := map[string]bool{}for := range .NumField() { := .Field()// handle embedded structsif .Anonymous { := .Typeif .Kind() == reflect.Ptr { = .Elem() }for := range () { [] = true }continue } := fieldJSONInfo()if !.omit { [.name] = true } }jsonNamesMap.Store(, )return}type jsonInfo struct { omit bool// unexported or first tag element is "-" name string// Go field name or first tag element. Empty if omit is true. settings map[string]bool// "omitempty", "omitzero", etc.}// fieldJSONInfo reports information about how encoding/json// handles the given struct field.// If the field is unexported, jsonInfo.omit is true and no other jsonInfo field// is populated.// If the field is exported and has no tag, then name is the field's name and all// other fields are false.// Otherwise, the information is obtained from the tag.func fieldJSONInfo( reflect.StructField) jsonInfo {if !.IsExported() {returnjsonInfo{omit: true} } := jsonInfo{name: .Name}if , := .Tag.Lookup("json"); { , , := strings.Cut(, ",")// "-" means omit, but "-," means the name is "-"if == "-" && ! {returnjsonInfo{omit: true} }if != "" { .name = }iflen() > 0 { .settings = map[string]bool{}for , := rangestrings.Split(, ",") { .settings[] = true } } }return}// wrapf wraps *errp with the given formatted message if *errp is not nil.func wrapf( *error, string, ...any) {if * != nil { * = fmt.Errorf("%s: %w", fmt.Sprintf(, ...), *) }}
The pages are generated with Goldsv0.8.4. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.