fix(go.sum): update ResolveSpec dependency to v1.0.87
CI / build-and-test (push) Failing after 1s
Release / release (push) Failing after 19m26s

This commit is contained in:
Hein
2026-06-23 13:17:16 +02:00
parent 0227912325
commit 1adf50e3db
2436 changed files with 1078758 additions and 114 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 JSON Schema Go Project Authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+76
View File
@@ -0,0 +1,76 @@
// 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 jsonschema
import "maps"
// An annotations tracks certain properties computed by keywords that are used by validation.
// ("Annotation" is the spec's term.)
// In particular, the unevaluatedItems and unevaluatedProperties keywords need to know which
// items and properties were evaluated (validated successfully).
type annotations struct {
allItems bool // all items were evaluated
endIndex int // 1+largest index evaluated by prefixItems
evaluatedIndexes map[int]bool // set of indexes evaluated by contains
allProperties bool // all properties were evaluated
evaluatedProperties map[string]bool // set of properties evaluated by various keywords
}
// noteIndex marks i as evaluated.
func (a *annotations) noteIndex(i int) {
if a.evaluatedIndexes == nil {
a.evaluatedIndexes = map[int]bool{}
}
a.evaluatedIndexes[i] = true
}
// noteEndIndex marks items with index less than end as evaluated.
func (a *annotations) noteEndIndex(end int) {
if end > a.endIndex {
a.endIndex = end
}
}
// noteProperty marks prop as evaluated.
func (a *annotations) noteProperty(prop string) {
if a.evaluatedProperties == nil {
a.evaluatedProperties = map[string]bool{}
}
a.evaluatedProperties[prop] = true
}
// noteProperties marks all the properties in props as evaluated.
func (a *annotations) noteProperties(props map[string]bool) {
a.evaluatedProperties = merge(a.evaluatedProperties, props)
}
// merge adds b's annotations to a.
// a must not be nil.
func (a *annotations) merge(b *annotations) {
if b == nil {
return
}
if b.allItems {
a.allItems = true
}
if b.endIndex > a.endIndex {
a.endIndex = b.endIndex
}
a.evaluatedIndexes = merge(a.evaluatedIndexes, b.evaluatedIndexes)
if b.allProperties {
a.allProperties = true
}
a.evaluatedProperties = merge(a.evaluatedProperties, b.evaluatedProperties)
}
// merge adds t's keys to s and returns s.
// If s is nil, it returns a copy of t.
func merge[K comparable](s, t map[K]bool) map[K]bool {
if s == nil {
return maps.Clone(t)
}
maps.Copy(s, t)
return s
}
+115
View File
@@ -0,0 +1,115 @@
// 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 jsonschema is an implementation of the [JSON Schema specification],
a JSON-based format for describing the structure of JSON data.
The package can be used to read schemas for code generation, and to validate
data using the draft 2020-12 and draft-07 specifications. Validation with
other drafts or custom meta-schemas is not supported.
Construct a [Schema] as you would any Go struct (for example, by writing
a struct literal), or unmarshal a JSON schema into a [Schema] in the usual
way (with [encoding/json], for instance). It can then be used for code
generation or other purposes without further processing.
You can also infer a schema from a Go struct.
# Resolution
A Schema can refer to other schemas, both inside and outside itself. These
references must be resolved before a schema can be used for validation.
Call [Schema.Resolve] to obtain a resolved schema (called a [Resolved]).
If the schema has external references, pass a [ResolveOptions] with a [Loader]
to load them. To validate default values in a schema, set
[ResolveOptions.ValidateDefaults] to true.
# Validation
Call [Resolved.Validate] to validate a JSON value. The value must be a
Go value that looks like the result of unmarshaling a JSON value into an
[any] or a struct. For example, the JSON value
{"name": "Al", "scores": [90, 80, 100]}
could be represented as the Go value
map[string]any{
"name": "Al",
"scores": []any{90, 80, 100},
}
or as a value of this type:
type Player struct {
Name string `json:"name"`
Scores []int `json:"scores"`
}
# Inference
The [For] function returns a [Schema] describing the given Go type.
Each field in the struct becomes a property of the schema.
The values of "json" tags are respected: the field's property name is taken
from the tag, and fields omitted from the JSON are omitted from the schema as
well.
For example, `jsonschema.For[Player]()` returns this schema:
{
"properties": {
"name": {
"type": "string"
},
"scores": {
"type": "array",
"items": {"type": "integer"}
}
"required": ["name", "scores"],
"additionalProperties": {"not": {}}
}
}
Use the "jsonschema" struct tag to provide a description for the property:
type Player struct {
Name string `json:"name" jsonschema:"player name"`
Scores []int `json:"scores" jsonschema:"scores of player's games"`
}
# Deviations from the specification
Regular expressions are processed with Go's regexp package, which differs
from ECMA 262, most significantly in not supporting back-references.
See [this table of differences] for more.
The "format" keyword described in [section 7 of the validation spec] is recorded
in the Schema, but is ignored during validation.
It does not even produce [annotations].
Use the "pattern" keyword instead: it will work more reliably across JSON Schema
implementations. See [learnjsonschema.com] for more recommendations about "format".
The content keywords described in [section 8 of the validation spec]
are recorded in the schema, but ignored during validation.
# Controlling behavior changes
Minor and patch releases of this package may introduce behavior changes as part
of bug fixes or correctness improvements. To help manage the impact of such
changes, the package allows you to access previous behaviors using the
`JSONSCHEMAGODEBUG` environment variable. The available settings are listed
below; additional options may be introduced in future releases.
- **typeschemasnull**: When set to `"1"`, the inferred schema for slices will
*not* include the `null` type alongside the array type. It will also avoid
adding `null` to non-native pointer types (such as `time.Time`). This restores
the behavior from versions prior to v0.3.0. The default behavior is to include
`null` in these cases.
[JSON Schema specification]: https://json-schema.org
[section 7 of the validation spec]: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.7
[section 8 of the validation spec]: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.8
[learnjsonschema.com]: https://www.learnjsonschema.com/2020-12/format-annotation/format/
[this table of differences]: https://github.com/dlclark/regexp2?tab=readme-ov-file#compare-regexp-and-regexp2
[annotations]: https://json-schema.org/draft/2020-12/json-schema-core#name-annotations
*/
package jsonschema
+399
View File
@@ -0,0 +1,399 @@
// 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.
// This file contains functions that infer a schema from a Go type.
package jsonschema
import (
"encoding"
"fmt"
"log/slog"
"maps"
"math"
"math/big"
"os"
"reflect"
"regexp"
"slices"
"time"
)
const debugEnv = "JSONSCHEMAGODEBUG"
// ForOptions are options for the [For] and [ForType] functions.
type ForOptions struct {
// If IgnoreInvalidTypes is true, fields that can't be represented as a JSON
// Schema are ignored instead of causing an error.
// This allows callers to adjust the resulting schema using custom knowledge.
// For example, an interface type where all the possible implementations are
// known can be described with "oneof".
IgnoreInvalidTypes bool
// TypeSchemas maps types to their schemas.
// If [For] encounters a type that is a key in this map, the
// corresponding value is used as the resulting schema (after cloning to
// ensure uniqueness).
// Types in this map override the default translations, as described
// in [For]'s documentation.
// PropertyOrder defined in these schemas will not be used in [For] or [ForType].
TypeSchemas map[reflect.Type]*Schema
}
// For constructs a JSON schema object for the given type argument.
// If non-nil, the provided options configure certain aspects of this contruction,
// described below.
// It translates Go types into compatible JSON schema types, as follows.
// These defaults can be overridden by [ForOptions.TypeSchemas].
//
// - Strings have schema type "string".
// - Bools have schema type "boolean".
// - Signed and unsigned integer types have schema type "integer".
// - Floating point types have schema type "number".
// - Slices and arrays have schema type "array", and a corresponding schema
// for items.
// - Maps with string key have schema type "object", and corresponding
// schema for additionalProperties.
// - Structs have schema type "object", and disallow additionalProperties.
// Their properties are derived from exported struct fields, using the
// struct field JSON name. Fields that are marked "omitempty" or "omitzero" are
// considered optional; all other fields become required properties.
// For structs, the PropertyOrder will be set to the field order.
// - Some types in the standard library that implement json.Marshaler
// translate to schemas that match the values to which they marshal.
// For example, [time.Time] translates to the schema for strings.
//
// For will return an error if there is a cycle in the types.
//
// By default, For returns an error if t contains (possibly recursively) any of the
// following Go types, as they are incompatible with the JSON schema spec.
// If [ForOptions.IgnoreInvalidTypes] is true, then these types are ignored instead.
// - maps with key other than 'string'
// - function types
// - channel types
// - complex numbers
// - unsafe pointers
//
// This function recognizes struct field tags named "jsonschema".
// A jsonschema tag on a field is used as the description for the corresponding property.
// For future compatibility, descriptions must not start with "WORD=", where WORD is a
// sequence of non-whitespace characters.
func For[T any](opts *ForOptions) (*Schema, error) {
if opts == nil {
opts = &ForOptions{}
}
schemas := maps.Clone(initialSchemaMap)
// Add types from the options. They override the default ones.
maps.Copy(schemas, opts.TypeSchemas)
s, err := forType(reflect.TypeFor[T](), map[reflect.Type]bool{}, opts.IgnoreInvalidTypes, schemas)
if err != nil {
var z T
return nil, fmt.Errorf("For[%T](): %w", z, err)
}
return s, nil
}
// ForType is like [For], but takes a [reflect.Type]
func ForType(t reflect.Type, opts *ForOptions) (*Schema, error) {
if opts == nil {
opts = &ForOptions{}
}
schemas := maps.Clone(initialSchemaMap)
// Add types from the options. They override the default ones.
maps.Copy(schemas, opts.TypeSchemas)
s, err := forType(t, map[reflect.Type]bool{}, opts.IgnoreInvalidTypes, schemas)
if err != nil {
return nil, fmt.Errorf("ForType(%s): %w", t, err)
}
return s, nil
}
// Helper to create a *float64 pointer from a value
func f64Ptr(f float64) *float64 {
return &f
}
func forType(t reflect.Type, seen map[reflect.Type]bool, ignore bool, schemas map[reflect.Type]*Schema) (*Schema, error) {
// Follow pointers: the schema for *T is almost the same as for T, except that
// an explicit JSON "null" is allowed for the pointer.
allowNull := false
for t.Kind() == reflect.Pointer {
allowNull = true
t = t.Elem()
}
// Check for cycles
// User defined types have a name, so we can skip those that are natively defined
if t.Name() != "" {
if seen[t] {
return nil, fmt.Errorf("cycle detected for type %v", t)
}
seen[t] = true
defer delete(seen, t)
}
if s := schemas[t]; s != nil {
cloned := s.CloneSchemas()
if os.Getenv(debugEnv) != "typeschemasnull=1" && allowNull {
if cloned.Type != "" {
cloned.Types = []string{"null", cloned.Type}
cloned.Type = ""
} else if !slices.Contains(cloned.Types, "null") {
cloned.Types = append([]string{"null"}, cloned.Types...)
}
}
return cloned, nil
}
var (
s = new(Schema)
err error
)
switch t.Kind() {
case reflect.Bool:
s.Type = "boolean"
case reflect.Int, reflect.Int64:
s.Type = "integer"
case reflect.Uint, reflect.Uint64, reflect.Uintptr:
s.Type = "integer"
s.Minimum = f64Ptr(0)
case reflect.Int8:
s.Type = "integer"
s.Minimum = f64Ptr(math.MinInt8)
s.Maximum = f64Ptr(math.MaxInt8)
case reflect.Uint8:
s.Type = "integer"
s.Minimum = f64Ptr(0)
s.Maximum = f64Ptr(math.MaxUint8)
case reflect.Int16:
s.Type = "integer"
s.Minimum = f64Ptr(math.MinInt16)
s.Maximum = f64Ptr(math.MaxInt16)
case reflect.Uint16:
s.Type = "integer"
s.Minimum = f64Ptr(0)
s.Maximum = f64Ptr(math.MaxUint16)
case reflect.Int32:
s.Type = "integer"
s.Minimum = f64Ptr(math.MinInt32)
s.Maximum = f64Ptr(math.MaxInt32)
case reflect.Uint32:
s.Type = "integer"
s.Minimum = f64Ptr(0)
s.Maximum = f64Ptr(math.MaxUint32)
case reflect.Float32, reflect.Float64:
s.Type = "number"
case reflect.Interface:
// Unrestricted
case reflect.Map:
if t.Key().Kind() != reflect.String && !t.Key().Implements(reflect.TypeFor[encoding.TextMarshaler]()) {
if ignore {
return nil, nil // ignore
}
return nil, fmt.Errorf("unsupported map key type %v", t.Key().Kind())
}
s.Type = "object"
s.AdditionalProperties, err = forType(t.Elem(), seen, ignore, schemas)
if err != nil {
return nil, fmt.Errorf("computing map value schema: %v", err)
}
if ignore && s.AdditionalProperties == nil {
// Ignore if the element type is invalid.
return nil, nil
}
case reflect.Slice, reflect.Array:
if os.Getenv(debugEnv) != "typeschemasnull=1" && t.Kind() == reflect.Slice {
s.Types = []string{"null", "array"}
} else {
s.Type = "array"
}
itemsSchema, err := forType(t.Elem(), seen, ignore, schemas)
if err != nil {
return nil, fmt.Errorf("computing element schema: %v", err)
}
if itemsSchema == nil {
return nil, nil
}
s.Items = itemsSchema
if ignore && s.Items == nil {
// Ignore if the element type is invalid.
return nil, nil
}
if t.Kind() == reflect.Array {
s.MinItems = Ptr(t.Len())
s.MaxItems = Ptr(t.Len())
}
case reflect.String:
s.Type = "string"
case reflect.Struct:
s.Type = "object"
// no additional properties are allowed
s.AdditionalProperties = falseSchema()
// If skipPath is non-nil, it is path to an anonymous field whose
// schema has been replaced by a known schema.
var skipPath []int
for _, field := range reflect.VisibleFields(t) {
if s.Properties == nil {
s.Properties = make(map[string]*Schema)
}
if field.Anonymous {
override := schemas[field.Type]
if override != nil {
// Type must be object, and only properties can be set.
if override.Type != "object" {
return nil, fmt.Errorf(`custom schema for embedded struct must have type "object", got %q`,
override.Type)
}
// Check that all keywords relevant for objects are absent, except properties.
ov := reflect.ValueOf(override).Elem()
for _, sfi := range schemaFieldInfos {
if sfi.sf.Name == "Type" || sfi.sf.Name == "Properties" {
continue
}
fv := ov.FieldByIndex(sfi.sf.Index)
if !fv.IsZero() {
return nil, fmt.Errorf(`overrides for embedded fields can have only "Type" and "Properties"; this has %q`, sfi.sf.Name)
}
}
skipPath = field.Index
keys := make([]string, 0, len(override.Properties))
for k := range override.Properties {
keys = append(keys, k)
}
slices.Sort(keys)
for _, name := range keys {
if _, ok := s.Properties[name]; !ok {
s.Properties[name] = override.Properties[name].CloneSchemas()
s.PropertyOrder = append(s.PropertyOrder, name)
}
}
}
continue
}
// Check to see if this field has been promoted from a replaced anonymous
// type.
if skipPath != nil {
skip := false
if len(field.Index) >= len(skipPath) {
skip = true
for i, index := range skipPath {
if field.Index[i] != index {
// If we're no longer in a subfield.
skip = false
break
}
}
}
if skip {
continue
} else {
// Anonymous fields are followed immediately by their promoted fields.
// Once we encounter a field that *isn't* promoted, we can stop
// checking.
skipPath = nil
}
}
info := fieldJSONInfo(field)
if info.omit {
continue
}
fs, err := forType(field.Type, seen, ignore, schemas)
if err != nil {
return nil, err
}
if ignore && fs == nil {
// Skip fields of invalid type.
continue
}
if tag, ok := field.Tag.Lookup("jsonschema"); ok {
if tag == "" {
return nil, fmt.Errorf("empty jsonschema tag on struct field %s.%s", t, field.Name)
}
if disallowedPrefixRegexp.MatchString(tag) {
return nil, fmt.Errorf("tag must not begin with 'WORD=': %q", tag)
}
fs.Description = tag
}
s.Properties[info.name] = fs
s.PropertyOrder = append(s.PropertyOrder, info.name)
if !info.settings["omitempty"] && !info.settings["omitzero"] {
s.Required = append(s.Required, info.name)
}
}
// Remove PropertyOrder duplicates, keeping the last occurrence
if len(s.PropertyOrder) > 1 {
seen := make(map[string]bool)
// Create a slice to hold the cleaned order (capacity = current length)
cleaned := make([]string, 0, len(s.PropertyOrder))
// Iterate backwards
for i := len(s.PropertyOrder) - 1; i >= 0; i-- {
name := s.PropertyOrder[i]
if !seen[name] {
cleaned = append(cleaned, name)
seen[name] = true
}
}
// Since we collected them backwards, we need to reverse the result
// to restore the correct order.
slices.Reverse(cleaned)
s.PropertyOrder = cleaned
}
default:
if ignore {
// Ignore.
return nil, nil
}
return nil, fmt.Errorf("type %v is unsupported by jsonschema", t)
}
if allowNull && s.Type != "" {
s.Types = []string{"null", s.Type}
s.Type = ""
}
return s, nil
}
// initialSchemaMap holds types from the standard library that have MarshalJSON methods.
var initialSchemaMap = make(map[reflect.Type]*Schema)
func init() {
ss := &Schema{Type: "string"}
initialSchemaMap[reflect.TypeFor[time.Time]()] = ss
initialSchemaMap[reflect.TypeFor[slog.Level]()] = ss
if os.Getenv(debugEnv) == "typeschemasnull=1" {
initialSchemaMap[reflect.TypeFor[big.Int]()] = &Schema{Types: []string{"null", "string"}}
} else {
initialSchemaMap[reflect.TypeFor[big.Int]()] = ss
}
initialSchemaMap[reflect.TypeFor[big.Rat]()] = ss
initialSchemaMap[reflect.TypeFor[big.Float]()] = ss
}
// Disallow jsonschema tag values beginning "WORD=", for future expansion.
var disallowedPrefixRegexp = regexp.MustCompile("^[^ \t\n]*=")
+160
View File
@@ -0,0 +1,160 @@
// 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.
// This file implements JSON Pointers.
// A JSON Pointer is a path that refers to one JSON value within another.
// If the path is empty, it refers to the root value.
// Otherwise, it is a sequence of slash-prefixed strings, like "/points/1/x",
// selecting successive properties (for JSON objects) or items (for JSON arrays).
// For example, when applied to this JSON value:
// {
// "points": [
// {"x": 1, "y": 2},
// {"x": 3, "y": 4}
// ]
// }
//
// the JSON Pointer "/points/1/x" refers to the number 3.
// See the spec at https://datatracker.ietf.org/doc/html/rfc6901.
package jsonschema
import (
"errors"
"fmt"
"reflect"
"strconv"
"strings"
)
var (
jsonPointerEscaper = strings.NewReplacer("~", "~0", "/", "~1")
jsonPointerUnescaper = strings.NewReplacer("~0", "~", "~1", "/")
)
func escapeJSONPointerSegment(s string) string {
return jsonPointerEscaper.Replace(s)
}
func unescapeJSONPointerSegment(s string) string {
return jsonPointerUnescaper.Replace(s)
}
// parseJSONPointer splits a JSON Pointer into a sequence of segments. It doesn't
// convert strings to numbers, because that depends on the traversal: a segment
// is treated as a number when applied to an array, but a string when applied to
// an object. See section 4 of the spec.
func parseJSONPointer(ptr string) (segments []string, err error) {
if ptr == "" {
return nil, nil
}
if ptr[0] != '/' {
return nil, fmt.Errorf("JSON Pointer %q does not begin with '/'", ptr)
}
// Unlike file paths, consecutive slashes are not coalesced.
// Split is nicer than Cut here, because it gets a final "/" right.
segments = strings.Split(ptr[1:], "/")
if strings.Contains(ptr, "~") {
// Undo the simple escaping rules that allow one to include a slash in a segment.
for i := range segments {
segments[i] = unescapeJSONPointerSegment(segments[i])
}
}
return segments, nil
}
// dereferenceJSONPointer returns the Schema that sptr points to within s,
// or an error if none.
// This implementation suffices for JSON Schema: pointers are applied only to Schemas,
// and refer only to Schemas.
func dereferenceJSONPointer(s *Schema, sptr string) (_ *Schema, err error) {
defer wrapf(&err, "JSON Pointer %q", sptr)
segments, err := parseJSONPointer(sptr)
if err != nil {
return nil, err
}
v := reflect.ValueOf(s)
for _, seg := range segments {
switch v.Kind() {
case reflect.Pointer:
v = v.Elem()
if !v.IsValid() {
return nil, errors.New("navigated to nil reference")
}
fallthrough // if valid, can only be a pointer to a Schema
case reflect.Struct:
// The segment must refer to a field in a Schema.
if v.Type() != reflect.TypeFor[Schema]() {
return nil, fmt.Errorf("navigated to non-Schema %s", v.Type())
}
v = lookupSchemaField(v, seg)
if !v.IsValid() {
return nil, fmt.Errorf("no schema field %q", seg)
}
case reflect.Slice, reflect.Array:
// The segment must be an integer without leading zeroes that refers to an item in the
// slice or array.
if seg == "-" {
return nil, errors.New("the JSON Pointer array segment '-' is not supported")
}
if len(seg) > 1 && seg[0] == '0' {
return nil, fmt.Errorf("segment %q has leading zeroes", seg)
}
n, err := strconv.Atoi(seg)
if err != nil {
return nil, fmt.Errorf("invalid int: %q", seg)
}
if n < 0 || n >= v.Len() {
return nil, fmt.Errorf("index %d is out of bounds for array of length %d", n, v.Len())
}
v = v.Index(n)
// Cannot be invalid.
case reflect.Map:
// The segment must be a key in the map.
v = v.MapIndex(reflect.ValueOf(seg))
if !v.IsValid() {
return nil, fmt.Errorf("no key %q in map", seg)
}
default:
return nil, fmt.Errorf("value %s (%s) is not a schema, slice or map", v, v.Type())
}
}
if s, ok := v.Interface().(*Schema); ok {
return s, nil
}
return nil, fmt.Errorf("does not refer to a schema, but to a %s", v.Type())
}
// lookupSchemaField returns the value of the field with the given name in v,
// or the zero value if there is no such field or it is not of type Schema or *Schema.
func lookupSchemaField(v reflect.Value, name string) reflect.Value {
if name == "type" {
// The "type" keyword may refer to Type or Types.
// At most one will be non-zero.
if t := v.FieldByName("Type"); !t.IsZero() {
return t
}
return v.FieldByName("Types")
}
if name == "items" {
// The "items" keyword refers to the "union type" that is either a schema or a schema array.
// Implemented using the Items representing the schema and ItemsArray for the schema array.
if items := v.FieldByName("Items"); items.IsValid() && !items.IsNil() {
return items
}
return v.FieldByName("ItemsArray")
}
if name == "dependencies" {
// The "dependencies" keyword refers to both DependencyStrings and DependencySchemas maps.
// The value on schemaFieldMap is not garanteed to be DependencySchemas which we want
// for pointer dereference. So we use FieldByName to get the DependencySchemas map.
return v.FieldByName("DependencySchemas")
}
if sf, ok := schemaFieldMap[name]; ok {
return v.FieldByIndex(sf.Index)
}
return reflect.Value{}
}
+589
View File
@@ -0,0 +1,589 @@
// 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.
// This file deals with preparing a schema for validation, including various checks,
// optimizations, and the resolution of cross-schema references.
package jsonschema
import (
"errors"
"fmt"
"net/url"
"reflect"
"regexp"
"strings"
)
// A Resolved consists of a [Schema] along with associated information needed to
// validate documents against it.
// A Resolved has been validated against its meta-schema, and all its references
// (the $ref and $dynamicRef keywords) have been resolved to their referenced Schemas.
// Call [Schema.Resolve] to obtain a Resolved from a Schema.
type Resolved struct {
root *Schema
draft draft
// map from $ids to their schemas
resolvedURIs map[string]*Schema
// map from schemas to additional info computed during resolution
resolvedInfos map[*Schema]*resolvedInfo
}
type draft int
const (
draft7 = iota
draft2020
)
func newResolved(s *Schema) *Resolved {
return &Resolved{
root: s,
draft: detectDraft(s),
resolvedURIs: map[string]*Schema{},
resolvedInfos: map[*Schema]*resolvedInfo{},
}
}
// detectDraft inspects the raw JSON to determine the schema version.
func detectDraft(s *Schema) draft {
// Check explicit $schema declaration
switch s.Schema {
case draft7SchemaVersion, draft7SecSchemaVersion:
return draft7
case draft202012SchemaVersion:
return draft2020
default:
// If nothing matches default to the latest supported version.
return draft2020
}
}
// resolvedInfo holds information specific to a schema that is computed by [Schema.Resolve].
type resolvedInfo struct {
s *Schema
// The JSON Pointer path from the root schema to here.
// Used in errors.
path string
// The schema's base schema.
// If the schema is the root or has an ID, its base is itself.
// Otherwise, its base is the innermost enclosing schema whose base
// is itself.
// Intuitively, a base schema is one that can be referred to with a
// fragmentless URI.
base *Schema
// The URI for the schema, if it is the root or has an ID.
// Otherwise nil.
// Invariants:
// s.base.uri != nil.
// s.base == s <=> s.uri != nil
uri *url.URL
// The schema to which Ref refers.
resolvedRef *Schema
// If the schema has a dynamic ref, exactly one of the next two fields
// will be non-zero after successful resolution.
// The schema to which the dynamic ref refers when it acts lexically.
resolvedDynamicRef *Schema
// The anchor to look up on the stack when the dynamic ref acts dynamically.
dynamicRefAnchor string
// The following fields are independent of arguments to Schema.Resolved,
// so they could live on the Schema. We put them here for simplicity.
// The set of required properties.
isRequired map[string]bool
// Compiled regexps.
pattern *regexp.Regexp
patternProperties map[*regexp.Regexp]*Schema
// Map from anchors to subschemas.
anchors map[string]anchorInfo
}
// Schema returns the schema that was resolved.
// It must not be modified.
func (r *Resolved) Schema() *Schema { return r.root }
// schemaString returns a short string describing the schema.
func (r *Resolved) schemaString(s *Schema) string {
if s.ID != "" {
return s.ID
}
info := r.resolvedInfos[s]
if info.path != "" {
return info.path
}
return "<anonymous schema>"
}
// A Loader reads and unmarshals the schema at uri, if any.
type Loader func(uri *url.URL) (*Schema, error)
// ResolveOptions are options for [Schema.Resolve].
type ResolveOptions struct {
// BaseURI is the URI relative to which the root schema should be resolved.
// If non-empty, must be an absolute URI (one that starts with a scheme).
// It is resolved (in the URI sense; see [url.ResolveReference]) with root's
// $id property.
// If the resulting URI is not absolute, then the schema cannot contain
// relative URI references.
BaseURI string
// Loader loads schemas that are referred to by a $ref but are not under the
// root schema (remote references).
// If nil, resolving a remote reference will return an error.
Loader Loader
// ValidateDefaults determines whether to validate values of "default" keywords
// against their schemas.
// The [JSON Schema specification] does not require this, but it is recommended
// if defaults will be used.
//
// [JSON Schema specification]: https://json-schema.org/understanding-json-schema/reference/annotations
ValidateDefaults bool
}
// Resolve resolves all references within the schema and performs other tasks that
// prepare the schema for validation.
// If opts is nil, the default values are used.
// The schema must not be changed after Resolve is called.
// The same schema may be resolved multiple times.
func (root *Schema) Resolve(opts *ResolveOptions) (*Resolved, error) {
// There are up to five steps required to prepare a schema to validate.
// 1. Load: read the schema from somewhere and unmarshal it.
// This schema (root) may have been loaded or created in memory, but other schemas that
// come into the picture in step 4 will be loaded by the given loader.
// 2. Check: validate the schema against a meta-schema, and perform other well-formedness checks.
// Precompute some values along the way.
// 3. Resolve URIs: determine the base URI of the root and all its subschemas, and
// resolve (in the URI sense) all identifiers and anchors with their bases. This step results
// in a map from URIs to schemas within root.
// 4. Resolve references: all refs in the schemas are replaced with the schema they refer to.
// 5. (Optional.) If opts.ValidateDefaults is true, validate the defaults.
r := &resolver{loaded: map[string]*Resolved{}}
if opts != nil {
r.opts = *opts
}
var base *url.URL
if r.opts.BaseURI == "" {
base = &url.URL{} // so we can call ResolveReference on it
} else {
var err error
base, err = url.Parse(r.opts.BaseURI)
if err != nil {
return nil, fmt.Errorf("parsing base URI: %w", err)
}
}
if r.opts.Loader == nil {
r.opts.Loader = func(uri *url.URL) (*Schema, error) {
return nil, errors.New("cannot resolve remote schemas: no loader passed to Schema.Resolve")
}
}
resolved, err := r.resolve(root, base)
if err != nil {
return nil, err
}
if r.opts.ValidateDefaults {
if err := resolved.validateDefaults(); err != nil {
return nil, err
}
}
// TODO: before we return, throw away anything we don't need for validation.
return resolved, nil
}
// A resolver holds the state for resolution.
type resolver struct {
opts ResolveOptions
// A cache of loaded and partly resolved schemas. (They may not have had their
// refs resolved.) The cache ensures that the loader will never be called more
// than once with the same URI, and that reference cycles are handled properly.
loaded map[string]*Resolved
}
func (r *resolver) resolve(s *Schema, baseURI *url.URL) (*Resolved, error) {
if baseURI.Fragment != "" {
return nil, fmt.Errorf("base URI %s must not have a fragment", baseURI)
}
rs := newResolved(s)
if err := s.check(rs.resolvedInfos); err != nil {
return nil, err
}
if err := resolveURIs(rs, baseURI); err != nil {
return nil, err
}
// Remember the schema by both the URI we loaded it from and its canonical name,
// which may differ if the schema has an $id.
// We must set the map before calling resolveRefs, or ref cycles will cause unbounded recursion.
r.loaded[baseURI.String()] = rs
r.loaded[rs.resolvedInfos[s].uri.String()] = rs
if err := r.resolveRefs(rs); err != nil {
return nil, err
}
return rs, nil
}
func (root *Schema) check(infos map[*Schema]*resolvedInfo) error {
// Check for structural validity. Do this first and fail fast:
// bad structure will cause other code to panic.
if err := root.checkStructure(infos); err != nil {
return err
}
var errs []error
report := func(err error) { errs = append(errs, err) }
for ss := range root.all() {
ss.checkLocal(report, infos)
}
return errors.Join(errs...)
}
// checkStructure verifies that root and its subschemas form a tree.
// It also assigns each schema a unique path, to improve error messages.
func (root *Schema) checkStructure(infos map[*Schema]*resolvedInfo) error {
assert(len(infos) == 0, "non-empty infos")
var check func(reflect.Value, []byte) error
check = func(v reflect.Value, path []byte) error {
// For the purpose of error messages, the root schema has path "root"
// and other schemas' paths are their JSON Pointer from the root.
p := "root"
if len(path) > 0 {
p = string(path)
}
s := v.Interface().(*Schema)
if s == nil {
return fmt.Errorf("jsonschema: schema at %s is nil", p)
}
if info, ok := infos[s]; ok {
// We've seen s before.
// The schema graph at root is not a tree, but it needs to
// be because a schema's base must be unique.
// A cycle would also put Schema.all into an infinite recursion.
return fmt.Errorf("jsonschema: schemas at %s do not form a tree; %s appears more than once (also at %s)",
root, info.path, p)
}
infos[s] = &resolvedInfo{s: s, path: p}
for _, info := range schemaFieldInfos {
fv := v.Elem().FieldByIndex(info.sf.Index)
switch info.sf.Type {
case schemaType:
// A field that contains an individual schema.
// A nil is valid: it just means the field isn't present.
if !fv.IsNil() {
if err := check(fv, fmt.Appendf(path, "/%s", info.jsonName)); err != nil {
return err
}
}
case schemaSliceType:
for i := range fv.Len() {
if err := check(fv.Index(i), fmt.Appendf(path, "/%s/%d", info.jsonName, i)); err != nil {
return err
}
}
case schemaMapType:
iter := fv.MapRange()
for iter.Next() {
key := escapeJSONPointerSegment(iter.Key().String())
if err := check(iter.Value(), fmt.Appendf(path, "/%s/%s", info.jsonName, key)); err != nil {
return err
}
}
}
}
return nil
}
return check(reflect.ValueOf(root), make([]byte, 0, 256))
}
// checkLocal checks s for validity, independently of other schemas it may refer to.
// Since checking a regexp involves compiling it, checkLocal saves those compiled regexps
// in the schema for later use.
// It appends the errors it finds to errs.
func (s *Schema) checkLocal(report func(error), infos map[*Schema]*resolvedInfo) {
addf := func(format string, args ...any) {
msg := fmt.Sprintf(format, args...)
report(fmt.Errorf("jsonschema.Schema: %s: %s", s, msg))
}
if s == nil {
addf("nil subschema")
return
}
if err := s.basicChecks(); err != nil {
report(err)
return
}
// TODO: validate the schema's properties,
// ideally by jsonschema-validating it against the meta-schema.
// Some properties are present so that Schemas can round-trip, but we do not
// validate them.
// Currently, it's just the $vocabulary property.
// As a special case, we can validate the 2020-12 meta-schema.
if s.Vocabulary != nil && s.Schema != draft202012SchemaVersion {
addf("cannot validate a schema with $vocabulary")
}
info := infos[s]
// Check and compile regexps.
if s.Pattern != "" {
re, err := regexp.Compile(s.Pattern)
if err != nil {
addf("pattern: %v", err)
} else {
info.pattern = re
}
}
if len(s.PatternProperties) > 0 {
info.patternProperties = map[*regexp.Regexp]*Schema{}
for reString, subschema := range s.PatternProperties {
re, err := regexp.Compile(reString)
if err != nil {
addf("patternProperties[%q]: %v", reString, err)
continue
}
info.patternProperties[re] = subschema
}
}
// Build a set of required properties, to avoid quadratic behavior when validating
// a struct.
if len(s.Required) > 0 {
info.isRequired = map[string]bool{}
for _, r := range s.Required {
info.isRequired[r] = true
}
}
}
// resolveURIs resolves the ids and anchors in all the schemas of root, relative
// to baseURI.
// See https://json-schema.org/draft/2020-12/json-schema-core#section-8.2, section
// 8.2.1.
//
// Every schema has a base URI and a parent base URI.
//
// The parent base URI is the base URI of the lexically enclosing schema, or for
// a root schema, the URI it was loaded from or the one supplied to [Schema.Resolve].
//
// If the schema has no $id property, the base URI of a schema is that of its parent.
// If the schema does have an $id, it must be a URI, possibly relative. The schema's
// base URI is the $id resolved (in the sense of [url.URL.ResolveReference]) against
// the parent base.
//
// As an example, consider this schema loaded from http://a.com/root.json (quotes omitted):
//
// {
// allOf: [
// {$id: "sub1.json", minLength: 5},
// {$id: "http://b.com", minimum: 10},
// {not: {maximum: 20}}
// ]
// }
//
// The base URIs are as follows. Schema locations are expressed in the JSON Pointer notation.
//
// schema base URI
// root http://a.com/root.json
// allOf/0 http://a.com/sub1.json
// allOf/1 http://b.com (absolute $id; doesn't matter that it's not under the loaded URI)
// allOf/2 http://a.com/root.json (inherited from parent)
// allOf/2/not http://a.com/root.json (inherited from parent)
func resolveURIs(rs *Resolved, baseURI *url.URL) error {
// Anchors and dynamic anchors are URI fragments that are scoped to their base.
// We treat them as keys in a map stored within the schema.
setAnchor := func(s *Schema, baseInfo *resolvedInfo, anchor string, dynamic bool) error {
if anchor != "" {
if _, ok := baseInfo.anchors[anchor]; ok {
return fmt.Errorf("duplicate anchor %q in %s", anchor, baseInfo.uri)
}
if baseInfo.anchors == nil {
baseInfo.anchors = map[string]anchorInfo{}
}
baseInfo.anchors[anchor] = anchorInfo{s, dynamic}
}
return nil
}
var resolve func(s, base *Schema) error
resolve = func(s, base *Schema) error {
info := rs.resolvedInfos[s]
baseInfo := rs.resolvedInfos[base]
// ids are scoped to the root.
if s.ID != "" {
// draft-7 specific
// https://json-schema.org/draft-07/draft-handrews-json-schema-01#rfc.section.8.3
// "All other properties in a "$ref" object MUST be ignored."
ignore := rs.draft == draft7 && s.Ref != ""
if !ignore {
// A non-empty ID establishes a new base.
idURI, err := url.Parse(s.ID)
if err != nil {
return err
}
if rs.draft == draft2020 && idURI.Fragment != "" {
return fmt.Errorf("$id %s must not have a fragment", s.ID)
}
if rs.draft == draft7 && idURI.Fragment != "" {
// anchor did not exist in draft 7, id was used for base uri and document navigation
// https://json-schema.org/draft-07/draft-handrews-json-schema-01#id-keyword
anchorName := strings.TrimPrefix(s.ID, "#")
setAnchor(s, baseInfo, anchorName, false)
} else {
// The base URI for this schema is its $id resolved against the parent base.
info.uri = baseInfo.uri.ResolveReference(idURI)
if !info.uri.IsAbs() {
return fmt.Errorf("$id %s does not resolve to an absolute URI (base is %q)", s.ID, baseInfo.uri)
}
rs.resolvedURIs[info.uri.String()] = s
base = s // needed for anchors
baseInfo = rs.resolvedInfos[base]
}
}
}
info.base = base
if rs.draft == draft2020 {
setAnchor(s, baseInfo, s.Anchor, false)
setAnchor(s, baseInfo, s.DynamicAnchor, true)
}
for c := range s.children() {
if err := resolve(c, base); err != nil {
return err
}
}
return nil
}
// Set the root URI to the base for now. If the root has an $id, this will change.
rs.resolvedInfos[rs.root].uri = baseURI
// The original base, even if changed, is still a valid way to refer to the root.
rs.resolvedURIs[baseURI.String()] = rs.root
return resolve(rs.root, rs.root)
}
// resolveRefs replaces every ref in the schemas with the schema it refers to.
// A reference that doesn't resolve within the schema may refer to some other schema
// that needs to be loaded.
func (r *resolver) resolveRefs(rs *Resolved) error {
for s := range rs.root.all() {
info := rs.resolvedInfos[s]
if s.Ref != "" {
refSchema, _, err := r.resolveRef(rs, s, s.Ref)
if err != nil {
return err
}
// Whether or not the anchor referred to by $ref fragment is dynamic,
// the ref still treats it lexically.
info.resolvedRef = refSchema
}
if s.DynamicRef != "" {
refSchema, frag, err := r.resolveRef(rs, s, s.DynamicRef)
if err != nil {
return err
}
if frag != "" {
// The dynamic ref's fragment points to a dynamic anchor.
// We must resolve the fragment at validation time.
info.dynamicRefAnchor = frag
} else {
// There is no dynamic anchor in the lexically referenced schema,
// so the dynamic ref behaves like a lexical ref.
info.resolvedDynamicRef = refSchema
}
}
}
return nil
}
// resolveRef resolves the reference ref, which is either s.Ref or s.DynamicRef.
func (r *resolver) resolveRef(rs *Resolved, s *Schema, ref string) (_ *Schema, dynamicFragment string, err error) {
refURI, err := url.Parse(ref)
if err != nil {
return nil, "", err
}
// URI-resolve the ref against the current base URI to get a complete URI.
base := rs.resolvedInfos[s].base
refURI = rs.resolvedInfos[base].uri.ResolveReference(refURI)
// The non-fragment part of a ref URI refers to the base URI of some schema.
// This part is the same for dynamic refs too: their non-fragment part resolves
// lexically.
u := *refURI
u.Fragment = ""
fraglessRefURI := &u
// Look it up locally.
referencedSchema := rs.resolvedURIs[fraglessRefURI.String()]
if referencedSchema == nil {
// The schema is remote. Maybe we've already loaded it.
// We assume that the non-fragment part of refURI refers to a top-level schema
// document. That is, we don't support the case exemplified by
// http://foo.com/bar.json/baz, where the document is in bar.json and
// the reference points to a subschema within it.
// TODO: support that case.
if lrs := r.loaded[fraglessRefURI.String()]; lrs != nil {
referencedSchema = lrs.root
} else {
// Try to load the schema.
ls, err := r.opts.Loader(fraglessRefURI)
if err != nil {
return nil, "", fmt.Errorf("loading %s: %w", fraglessRefURI, err)
}
// Check if referenced schema has $schema defined. If not it should inherit the resolved
if ls.Schema == "" {
ls.Schema = s.Schema
}
lrs, err := r.resolve(ls, fraglessRefURI)
if err != nil {
return nil, "", err
}
referencedSchema = lrs.root
assert(referencedSchema != nil, "nil referenced schema")
// Copy the resolvedInfos from lrs into rs, without overwriting
// (hence we can't use maps.Insert).
for s, i := range lrs.resolvedInfos {
if rs.resolvedInfos[s] == nil {
rs.resolvedInfos[s] = i
}
}
}
}
frag := refURI.Fragment
// Look up frag in refSchema.
// frag is either a JSON Pointer or the name of an anchor.
// A JSON Pointer is either the empty string or begins with a '/',
// whereas anchors are always non-empty strings that don't contain slashes.
if frag != "" && !strings.HasPrefix(frag, "/") {
resInfo := rs.resolvedInfos[referencedSchema]
info, found := resInfo.anchors[frag]
if !found {
return nil, "", fmt.Errorf("no anchor %q in %s", frag, s)
}
if info.dynamic {
dynamicFragment = frag
}
return info.schema, dynamicFragment, nil
}
// frag is a JSON Pointer.
s, err = dereferenceJSONPointer(referencedSchema, frag)
return s, "", err
}
+642
View File
@@ -0,0 +1,642 @@
// 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 jsonschema
import (
"bytes"
"cmp"
"encoding/json"
"errors"
"fmt"
"iter"
"maps"
"math"
"reflect"
"slices"
)
// A Schema is a JSON schema object.
// It supports both draft-07 and the 2020-12 draft specifications:
// - Draft-07: https://json-schema.org/draft-07/draft-handrews-json-schema-01
// and https://json-schema.org/draft-07/draft-handrews-json-schema-validation-01
// - Draft 2020-12: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-01
// and https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01
//
// A Schema value may have non-zero values for more than one field:
// all relevant non-zero fields are used for validation.
// There is one exception to provide more Go type-safety: the Type and Types fields
// are mutually exclusive.
//
// Since this struct is a Go representation of a JSON value, it inherits JSON's
// distinction between nil and empty. Nil slices and maps are considered absent,
// but empty ones are present and affect validation. For example,
//
// Schema{Enum: nil}
//
// is equivalent to an empty schema, so it validates every instance. But
//
// Schema{Enum: []any{}}
//
// requires equality to some slice element, so it vacuously rejects every instance.
type Schema struct {
// core
ID string `json:"$id,omitempty"`
Schema string `json:"$schema,omitempty"`
Ref string `json:"$ref,omitempty"`
Comment string `json:"$comment,omitempty"`
Defs map[string]*Schema `json:"$defs,omitempty"`
Definitions map[string]*Schema `json:"definitions,omitempty"`
// split draft 7 Dependencies into DependencySchemas and DependencyStrings
DependencySchemas map[string]*Schema `json:"-"`
DependencyStrings map[string][]string `json:"-"`
Anchor string `json:"$anchor,omitempty"`
DynamicAnchor string `json:"$dynamicAnchor,omitempty"`
DynamicRef string `json:"$dynamicRef,omitempty"`
Vocabulary map[string]bool `json:"$vocabulary,omitempty"`
// metadata
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
Default json.RawMessage `json:"default,omitempty"`
Deprecated bool `json:"deprecated,omitempty"`
ReadOnly bool `json:"readOnly,omitempty"`
WriteOnly bool `json:"writeOnly,omitempty"`
Examples []any `json:"examples,omitempty"`
// validation
// Use Type for a single type, or Types for multiple types; never both.
Type string `json:"-"`
Types []string `json:"-"`
Enum []any `json:"enum,omitempty"`
// Const is *any because a JSON null (Go nil) is a valid value.
Const *any `json:"const,omitempty"`
MultipleOf *float64 `json:"multipleOf,omitempty"`
Minimum *float64 `json:"minimum,omitempty"`
Maximum *float64 `json:"maximum,omitempty"`
ExclusiveMinimum *float64 `json:"exclusiveMinimum,omitempty"`
ExclusiveMaximum *float64 `json:"exclusiveMaximum,omitempty"`
MinLength *int `json:"minLength,omitempty"`
MaxLength *int `json:"maxLength,omitempty"`
Pattern string `json:"pattern,omitempty"`
// arrays
PrefixItems []*Schema `json:"prefixItems,omitempty"`
Items *Schema `json:"-"`
ItemsArray []*Schema `json:"-"`
MinItems *int `json:"minItems,omitempty"`
MaxItems *int `json:"maxItems,omitempty"`
AdditionalItems *Schema `json:"additionalItems,omitempty"`
UniqueItems bool `json:"uniqueItems,omitempty"`
Contains *Schema `json:"contains,omitempty"`
MinContains *int `json:"minContains,omitempty"` // *int, not int: default is 1, not 0
MaxContains *int `json:"maxContains,omitempty"`
UnevaluatedItems *Schema `json:"unevaluatedItems,omitempty"`
// objects
MinProperties *int `json:"minProperties,omitempty"`
MaxProperties *int `json:"maxProperties,omitempty"`
Required []string `json:"required,omitempty"`
DependentRequired map[string][]string `json:"dependentRequired,omitempty"`
Properties map[string]*Schema `json:"properties,omitempty"`
PatternProperties map[string]*Schema `json:"patternProperties,omitempty"`
AdditionalProperties *Schema `json:"additionalProperties,omitempty"`
PropertyNames *Schema `json:"propertyNames,omitempty"`
UnevaluatedProperties *Schema `json:"unevaluatedProperties,omitempty"`
// logic
AllOf []*Schema `json:"allOf,omitempty"`
AnyOf []*Schema `json:"anyOf,omitempty"`
OneOf []*Schema `json:"oneOf,omitempty"`
Not *Schema `json:"not,omitempty"`
// conditional
If *Schema `json:"if,omitempty"`
Then *Schema `json:"then,omitempty"`
Else *Schema `json:"else,omitempty"`
DependentSchemas map[string]*Schema `json:"dependentSchemas,omitempty"`
// other
// https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.8
ContentEncoding string `json:"contentEncoding,omitempty"`
ContentMediaType string `json:"contentMediaType,omitempty"`
ContentSchema *Schema `json:"contentSchema,omitempty"`
// https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.7
Format string `json:"format,omitempty"`
// Extra allows for additional keywords beyond those specified.
Extra map[string]any `json:"-"`
// PropertyOrder records the ordering of properties for JSON rendering.
//
// During [For], PropertyOrder is set to the field order,
// if the type used for inference is a struct.
//
// If PropertyOrder is set, it controls the relative ordering of properties in [Schema.MarshalJSON].
// The rendered JSON first lists any properties that appear in the PropertyOrder slice in the order
// they appear, followed by all other properties that do not appear in the PropertyOrder slice in an
// undefined but deterministic order.
PropertyOrder []string `json:"-"`
}
// falseSchema returns a new Schema tree that fails to validate any value.
func falseSchema() *Schema {
return &Schema{Not: &Schema{}}
}
// anchorInfo records the subschema to which an anchor refers, and whether
// the anchor keyword is $anchor or $dynamicAnchor.
type anchorInfo struct {
schema *Schema
dynamic bool
}
// String returns a short description of the schema.
func (s *Schema) String() string {
if s.ID != "" {
return s.ID
}
if a := cmp.Or(s.Anchor, s.DynamicAnchor); a != "" {
return fmt.Sprintf("anchor %s", a)
}
return "<anonymous schema>"
}
// CloneSchemas returns a copy of s.
// The copy is shallow except for sub-schemas, which are themelves copied with CloneSchemas.
// This allows both s and s.CloneSchemas() to appear as sub-schemas of the same parent.
func (s *Schema) CloneSchemas() *Schema {
if s == nil {
return nil
}
s2 := *s
v := reflect.ValueOf(&s2)
for _, info := range schemaFieldInfos {
fv := v.Elem().FieldByIndex(info.sf.Index)
switch info.sf.Type {
case schemaType:
sscss := fv.Interface().(*Schema)
fv.Set(reflect.ValueOf(sscss.CloneSchemas()))
case schemaSliceType:
slice := fv.Interface().([]*Schema)
slice = slices.Clone(slice)
for i, ss := range slice {
slice[i] = ss.CloneSchemas()
}
fv.Set(reflect.ValueOf(slice))
case schemaMapType:
m := fv.Interface().(map[string]*Schema)
m = maps.Clone(m)
for k, ss := range m {
m[k] = ss.CloneSchemas()
}
fv.Set(reflect.ValueOf(m))
}
}
return &s2
}
func (s *Schema) basicChecks() error {
if s.Type != "" && s.Types != nil {
return errors.New("both Type and Types are set; at most one should be")
}
if s.Defs != nil && s.Definitions != nil {
return errors.New("both Defs and Definitions are set; at most one should be")
}
if s.Items != nil && s.ItemsArray != nil {
return errors.New("both Items and ItemsArray are set; at most one should be")
}
propertyOrderSeen := make(map[string]bool)
for _, val := range s.PropertyOrder {
if _, ok := propertyOrderSeen[val]; ok {
// Duplicate found
return fmt.Errorf("property order slice cannot contain duplicate entries, found duplicate %q", val)
}
propertyOrderSeen[val] = true
}
for key := range s.DependencySchemas {
// Check if the key exists in the dependency strings map
if _, exists := s.DependencyStrings[key]; exists {
return fmt.Errorf("dependency key %q cannot be defined as both a schema and a string array", key)
}
}
return nil
}
type schemaWithoutMethods Schema // doesn't implement json.{Unm,M}arshaler
func (s Schema) MarshalJSON() ([]byte, error) {
// NOTE: Use a value receiver here to avoid the encoding/json bugs
// described in golang/go#22967, golang/go#33993, and golang/go#55890.
// With a pointer receiver, MarshalJSON is only called for Schema in
// some cases (for example when the field value is addressable, or not
// stored as a map value), which leads to inconsistent JSON encoding.
// A value receiver makes Schema itself implement json.Marshaler and
// ensures that encoding/json always calls this method.
if err := s.basicChecks(); err != nil {
return nil, err
}
// Marshal either Type or Types as "type".
var typ any
switch {
case s.Type != "":
typ = s.Type
case s.Types != nil:
typ = s.Types
}
var items any
switch {
case s.Items != nil:
items = s.Items
case s.ItemsArray != nil:
items = s.ItemsArray
}
var dep map[string]any
size := len(s.DependencySchemas) + len(s.DependencyStrings)
if size > 0 {
dep = make(map[string]any, size)
for k, v := range s.DependencySchemas {
dep[k] = v
}
for k, v := range s.DependencyStrings {
dep[k] = v
}
}
ms := struct {
Type any `json:"type,omitempty"`
Properties json.Marshaler `json:"properties,omitempty"`
Dependencies map[string]any `json:"dependencies,omitempty"`
Items any `json:"items,omitempty"`
*schemaWithoutMethods
}{
Type: typ,
Dependencies: dep,
Items: items,
schemaWithoutMethods: (*schemaWithoutMethods)(&s),
}
// Marshal properties, even if the empty map (but not nil).
if s.Properties != nil {
ms.Properties = orderedProperties{
props: s.Properties,
order: s.PropertyOrder,
}
}
bs, err := marshalStructWithMap(&ms, "Extra")
if err != nil {
return nil, err
}
// Marshal {} as true and {"not": {}} as false.
// It is wasteful to do this here instead of earlier, but much easier.
switch {
case bytes.Equal(bs, []byte(`{}`)):
bs = []byte("true")
case bytes.Equal(bs, []byte(`{"not":true}`)):
bs = []byte("false")
}
return bs, nil
}
// orderedProperties is a helper to marshal the properties map in a specific order.
type orderedProperties struct {
props map[string]*Schema
order []string
}
func (op orderedProperties) MarshalJSON() ([]byte, error) {
var buf bytes.Buffer
buf.WriteByte('{')
first := true
processed := make(map[string]bool, len(op.props))
// Helper closure to write "key": value
writeEntry := func(key string, val *Schema) error {
if !first {
buf.WriteByte(',')
}
first = false
// Marshal the Key
keyBytes, err := json.Marshal(key)
if err != nil {
return err
}
buf.Write(keyBytes)
buf.WriteByte(':')
// Marshal the Value
valBytes, err := json.Marshal(val)
if err != nil {
return err
}
buf.Write(valBytes)
return nil
}
// Write keys explicitly listed in PropertyOrder
for _, name := range op.order {
if prop, ok := op.props[name]; ok {
if err := writeEntry(name, prop); err != nil {
return nil, err
}
processed[name] = true
}
}
// Write any remaining keys
var remaining []string
for name := range op.props {
if !processed[name] {
remaining = append(remaining, name)
}
}
// Sort the slice alphabetically
slices.Sort(remaining)
for _, name := range remaining {
if err := writeEntry(name, op.props[name]); err != nil {
return nil, err
}
}
buf.WriteByte('}')
return buf.Bytes(), nil
}
func (s *Schema) UnmarshalJSON(data []byte) error {
// A JSON boolean is a valid schema.
var b bool
if err := json.Unmarshal(data, &b); err == nil {
if b {
// true is the empty schema, which validates everything.
*s = Schema{}
} else {
// false is the schema that validates nothing.
*s = *falseSchema()
}
return nil
}
ms := struct {
Type json.RawMessage `json:"type,omitempty"`
Dependencies map[string]json.RawMessage `json:"dependencies,omitempty"`
Items json.RawMessage `json:"items,omitempty"`
Const json.RawMessage `json:"const,omitempty"`
MinLength *integer `json:"minLength,omitempty"`
MaxLength *integer `json:"maxLength,omitempty"`
MinItems *integer `json:"minItems,omitempty"`
MaxItems *integer `json:"maxItems,omitempty"`
MinProperties *integer `json:"minProperties,omitempty"`
MaxProperties *integer `json:"maxProperties,omitempty"`
MinContains *integer `json:"minContains,omitempty"`
MaxContains *integer `json:"maxContains,omitempty"`
*schemaWithoutMethods
}{
schemaWithoutMethods: (*schemaWithoutMethods)(s),
}
if err := unmarshalStructWithMap(data, &ms, "Extra"); err != nil {
return err
}
// Unmarshal "type" as either Type or Types.
var err error
if len(ms.Type) > 0 {
switch ms.Type[0] {
case '"':
err = json.Unmarshal(ms.Type, &s.Type)
case '[':
err = json.Unmarshal(ms.Type, &s.Types)
default:
err = fmt.Errorf(`invalid value for "type": %q`, ms.Type)
}
}
if err != nil {
return err
}
// Unmarshal "items" as either Items or ItemsArray.
if len(ms.Items) > 0 {
switch ms.Items[0] {
case '[':
var schemas []*Schema
err = json.Unmarshal(ms.Items, &schemas)
s.ItemsArray = schemas
default:
var schema Schema
err = json.Unmarshal(ms.Items, &schema)
s.Items = &schema
}
}
if err != nil {
return err
}
// Unmarshal "Dependencies" values as either string arrays or schemas
// and assign them to specific map DependencySchemas or DependencyStrings.
for k, v := range ms.Dependencies {
if len(v) > 0 {
switch v[0] {
case '[':
var dstrings []string
err = json.Unmarshal(v, &dstrings)
if s.DependencyStrings == nil {
s.DependencyStrings = make(map[string][]string)
}
s.DependencyStrings[k] = dstrings
default:
var dschema Schema
err = json.Unmarshal(v, &dschema)
if s.DependencySchemas == nil {
s.DependencySchemas = make(map[string]*Schema)
}
s.DependencySchemas[k] = &dschema
}
}
if err != nil {
return err
}
}
unmarshalAnyPtr := func(p **any, raw json.RawMessage) error {
if len(raw) == 0 {
return nil
}
if bytes.Equal(raw, []byte("null")) {
*p = new(any)
return nil
}
return json.Unmarshal(raw, p)
}
// Setting Const to a pointer to null will marshal properly, but won't
// unmarshal: the *any is set to nil, not a pointer to nil.
if err := unmarshalAnyPtr(&s.Const, ms.Const); err != nil {
return err
}
set := func(dst **int, src *integer) {
if src != nil {
*dst = Ptr(int(*src))
}
}
set(&s.MinLength, ms.MinLength)
set(&s.MaxLength, ms.MaxLength)
set(&s.MinItems, ms.MinItems)
set(&s.MaxItems, ms.MaxItems)
set(&s.MinProperties, ms.MinProperties)
set(&s.MaxProperties, ms.MaxProperties)
set(&s.MinContains, ms.MinContains)
set(&s.MaxContains, ms.MaxContains)
return nil
}
type integer int32 // for the integer-valued fields of Schema
func (ip *integer) UnmarshalJSON(data []byte) error {
if len(data) == 0 {
// nothing to do
return nil
}
// If there is a decimal point, src is a floating-point number.
var i int64
if bytes.ContainsRune(data, '.') {
var f float64
if err := json.Unmarshal(data, &f); err != nil {
return errors.New("not a number")
}
i = int64(f)
if float64(i) != f {
return errors.New("not an integer value")
}
} else {
if err := json.Unmarshal(data, &i); err != nil {
return errors.New("cannot be unmarshaled into an int")
}
}
// Ensure behavior is the same on both 32-bit and 64-bit systems.
if i < math.MinInt32 || i > math.MaxInt32 {
return errors.New("integer is out of range")
}
*ip = integer(i)
return nil
}
// Ptr returns a pointer to a new variable whose value is x.
func Ptr[T any](x T) *T { return &x }
// every applies f preorder to every schema under s including s.
// The second argument to f is the path to the schema appended to the argument path.
// It stops when f returns false.
func (s *Schema) every(f func(*Schema) bool) bool {
return f(s) && s.everyChild(func(s *Schema) bool { return s.every(f) })
}
// everyChild reports whether f is true for every immediate child schema of s.
func (s *Schema) everyChild(f func(*Schema) bool) bool {
v := reflect.ValueOf(s)
for _, info := range schemaFieldInfos {
fv := v.Elem().FieldByIndex(info.sf.Index)
switch info.sf.Type {
case schemaType:
// A field that contains an individual schema. A nil is valid: it just means the field isn't present.
c := fv.Interface().(*Schema)
if c != nil && !f(c) {
return false
}
case schemaSliceType:
slice := fv.Interface().([]*Schema)
for _, c := range slice {
if !f(c) {
return false
}
}
case schemaMapType:
// Sort keys for determinism.
m := fv.Interface().(map[string]*Schema)
for _, k := range slices.Sorted(maps.Keys(m)) {
if !f(m[k]) {
return false
}
}
}
}
return true
}
// all wraps every in an iterator.
func (s *Schema) all() iter.Seq[*Schema] {
return func(yield func(*Schema) bool) { s.every(yield) }
}
// children wraps everyChild in an iterator.
func (s *Schema) children() iter.Seq[*Schema] {
return func(yield func(*Schema) bool) { s.everyChild(yield) }
}
var (
schemaType = reflect.TypeFor[*Schema]()
schemaSliceType = reflect.TypeFor[[]*Schema]()
schemaMapType = reflect.TypeFor[map[string]*Schema]()
)
type structFieldInfo struct {
sf reflect.StructField
jsonName string
}
var (
// the visible fields of Schema that have a JSON name, sorted by that name
schemaFieldInfos []structFieldInfo
// map from JSON name to field
schemaFieldMap = map[string]reflect.StructField{}
)
func init() {
t := reflect.VisibleFields(reflect.TypeFor[Schema]())
for _, sf := range t {
info := fieldJSONInfo(sf)
if !info.omit {
schemaFieldInfos = append(schemaFieldInfos, structFieldInfo{sf, info.name})
} else {
// jsoninfo.name is used to build the info paths. The items and dependencies are ommited,
// since the original fields are separated to handle the union types supported in json and
// these fields have custom marshalling and unmarshalling logic.
// we still need these fields in schemaFieldInfos for creating schema trees and calculating paths and refs.
// so we manually create them and assign the jsonName to the original field json name.
switch sf.Name {
case "Items", "ItemsArray":
schemaFieldInfos = append(schemaFieldInfos, structFieldInfo{sf, "items"})
case "DependencySchemas", "DependencyStrings":
schemaFieldInfos = append(schemaFieldInfos, structFieldInfo{sf, "dependencies"})
}
}
}
// The value of "dependencies" this sort of schemaFieldInfos.
// This sort is unstable and is comparing the json.names of DependencyStrings and DependencySchemas which are both "dependencies".
// Since the sort is unstable it cannot be guarantied that "dependencies" has the DependencySchemas value.
slices.SortFunc(schemaFieldInfos, func(i1, i2 structFieldInfo) int {
return cmp.Compare(i1.jsonName, i2.jsonName)
})
for _, info := range schemaFieldInfos {
schemaFieldMap[info.jsonName] = info.sf
}
}
+463
View File
@@ -0,0 +1,463 @@
// 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 jsonschema
import (
"bytes"
"cmp"
"encoding/binary"
"encoding/json"
"fmt"
"hash/maphash"
"math"
"math/big"
"reflect"
"slices"
"strings"
"sync"
)
// 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 Equal(x, y any) bool {
return equalValue(reflect.ValueOf(x), reflect.ValueOf(y))
}
func equalValue(x, y reflect.Value) bool {
// Copied from src/reflect/deepequal.go, omitting the visited check (because JSON
// values are trees).
if !x.IsValid() || !y.IsValid() {
return x.IsValid() == y.IsValid()
}
// Treat numbers specially.
rx, ok1 := jsonNumber(x)
ry, ok2 := jsonNumber(y)
if ok1 && ok2 {
return rx.Cmp(ry) == 0
}
if x.Kind() != y.Kind() {
return false
}
switch x.Kind() {
case reflect.Array:
if x.Len() != y.Len() {
return false
}
for i := range x.Len() {
if !equalValue(x.Index(i), y.Index(i)) {
return false
}
}
return true
case reflect.Slice:
if x.IsNil() != y.IsNil() {
return false
}
if x.Len() != y.Len() {
return false
}
if x.UnsafePointer() == y.UnsafePointer() {
return true
}
// Special case for []byte, which is common.
if x.Type().Elem().Kind() == reflect.Uint8 && x.Type() == y.Type() {
return bytes.Equal(x.Bytes(), y.Bytes())
}
for i := range x.Len() {
if !equalValue(x.Index(i), y.Index(i)) {
return false
}
}
return true
case reflect.Interface:
if x.IsNil() || y.IsNil() {
return x.IsNil() == y.IsNil()
}
return equalValue(x.Elem(), y.Elem())
case reflect.Pointer:
if x.UnsafePointer() == y.UnsafePointer() {
return true
}
return equalValue(x.Elem(), y.Elem())
case reflect.Struct:
t := x.Type()
if t != y.Type() {
return false
}
for i := range t.NumField() {
sf := t.Field(i)
if !sf.IsExported() {
continue
}
if !equalValue(x.FieldByIndex(sf.Index), y.FieldByIndex(sf.Index)) {
return false
}
}
return true
case reflect.Map:
if x.IsNil() != y.IsNil() {
return false
}
if x.Len() != y.Len() {
return false
}
if x.UnsafePointer() == y.UnsafePointer() {
return true
}
iter := x.MapRange()
for iter.Next() {
vx := iter.Value()
vy := y.MapIndex(iter.Key())
if !vy.IsValid() || !equalValue(vx, vy) {
return false
}
}
return true
case reflect.Func:
if x.Type() != y.Type() {
return false
}
if x.IsNil() && y.IsNil() {
return true
}
panic("cannot compare functions")
case reflect.String:
return x.String() == y.String()
case reflect.Bool:
return x.Bool() == y.Bool()
// Ints, uints and floats handled in jsonNumber, at top of function.
default:
panic(fmt.Sprintf("unsupported kind: %s", x.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(h *maphash.Hash, v reflect.Value) {
// TODO: replace writes of basic types with WriteComparable in 1.24.
writeUint := func(u uint64) {
var buf [8]byte
binary.BigEndian.PutUint64(buf[:], u)
h.Write(buf[:])
}
var write func(reflect.Value)
write = func(v reflect.Value) {
if r, ok := jsonNumber(v); ok {
// 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.
writeUint(uint64(r.Sign() + 1))
h.Write(r.Num().Bytes())
h.Write(r.Denom().Bytes())
return
}
switch v.Kind() {
case reflect.Invalid:
h.WriteByte(0)
case reflect.String:
h.WriteString(v.String())
case reflect.Bool:
if v.Bool() {
h.WriteByte(1)
} else {
h.WriteByte(0)
}
case reflect.Complex64, reflect.Complex128:
c := v.Complex()
writeUint(math.Float64bits(real(c)))
writeUint(math.Float64bits(imag(c)))
case reflect.Array, reflect.Slice:
// Although we could treat []byte more efficiently,
// JSON values are unlikely to contain them.
writeUint(uint64(v.Len()))
for i := range v.Len() {
write(v.Index(i))
}
case reflect.Interface, reflect.Pointer:
write(v.Elem())
case reflect.Struct:
t := v.Type()
for i := range t.NumField() {
if sf := t.Field(i); sf.IsExported() {
write(v.FieldByIndex(sf.Index))
}
}
case reflect.Map:
if v.Type().Key().Kind() != reflect.String {
panic("map with non-string key")
}
// Sort the keys so the hash is deterministic.
keys := v.MapKeys()
// Write the length. That distinguishes between, say, two consecutive
// maps with disjoint keys from one map that has the items of both.
writeUint(uint64(len(keys)))
slices.SortFunc(keys, func(x, y reflect.Value) int { return cmp.Compare(x.String(), y.String()) })
for _, k := range keys {
write(k)
write(v.MapIndex(k))
}
// Ints, uints and floats handled in jsonNumber, at top of function.
default:
panic(fmt.Sprintf("unsupported kind: %s", v.Kind()))
}
}
write(v)
}
// 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(v reflect.Value) (*big.Rat, bool) {
r := new(big.Rat)
switch {
case !v.IsValid():
return nil, false
case v.CanInt():
r.SetInt64(v.Int())
case v.CanUint():
r.SetUint64(v.Uint())
case v.CanFloat():
r.SetFloat64(v.Float())
default:
jn, ok := v.Interface().(json.Number)
if !ok {
return nil, false
}
if _, ok := r.SetString(jn.String()); !ok {
// 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.
return nil, false
}
}
return r, 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(v reflect.Value) (string, bool) {
if !v.IsValid() {
// Not v.IsNil(): a nil []any is still a JSON array.
return "null", true
}
if v.CanInt() || v.CanUint() {
return "integer", true
}
if v.CanFloat() {
if _, f := math.Modf(v.Float()); f == 0 {
return "integer", true
}
return "number", true
}
switch v.Kind() {
case reflect.Bool:
return "boolean", true
case reflect.String:
return "string", true
case reflect.Slice, reflect.Array:
return "array", true
case reflect.Map, reflect.Struct:
return "object", true
default:
return "", false
}
}
func assert(cond bool, msg string) {
if !cond {
panic("assertion failed: " + msg)
}
}
// 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[T any](s *T, mapField 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 s == nil {
return []byte("null"), nil
}
s2 := *s
vMapField := reflect.ValueOf(&s2).Elem().FieldByName(mapField)
mapVal := vMapField.Interface().(map[string]any)
// Check for duplicates.
names := jsonNames(reflect.TypeFor[T]())
for key := range mapVal {
if names[key] {
return nil, fmt.Errorf("map key %q duplicates struct field", key)
}
}
structBytes, err := json.Marshal(s2)
if err != nil {
return nil, fmt.Errorf("marshalStructWithMap(%+v): %w", s, err)
}
if len(mapVal) == 0 {
return structBytes, nil
}
mapBytes, err := json.Marshal(mapVal)
if err != nil {
return nil, err
}
if len(structBytes) == 2 { // must be "{}"
return mapBytes, nil
}
// "{X}" + "{Y}" => "{X,Y}"
res := append(structBytes[:len(structBytes)-1], ',')
res = append(res, mapBytes[1:]...)
return res, 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[T any](data []byte, v *T, mapField string) error {
// Unmarshal into the struct, ignoring unknown fields.
if err := json.Unmarshal(data, v); err != nil {
return err
}
// Unmarshal into the map.
m := map[string]any{}
if err := json.Unmarshal(data, &m); err != nil {
return err
}
// Delete from the map the fields of the struct.
for n := range jsonNames(reflect.TypeFor[T]()) {
delete(m, n)
}
if len(m) != 0 {
reflect.ValueOf(v).Elem().FieldByName(mapField).Set(reflect.ValueOf(m))
}
return nil
}
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(t reflect.Type) map[string]bool {
// Lock not necessary: at worst we'll duplicate work.
if val, ok := jsonNamesMap.Load(t); ok {
return val.(map[string]bool)
}
m := map[string]bool{}
for i := range t.NumField() {
field := t.Field(i)
// handle embedded structs
if field.Anonymous {
fieldType := field.Type
if fieldType.Kind() == reflect.Ptr {
fieldType = fieldType.Elem()
}
for n := range jsonNames(fieldType) {
m[n] = true
}
continue
}
info := fieldJSONInfo(field)
if !info.omit {
m[info.name] = true
}
}
jsonNamesMap.Store(t, m)
return m
}
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(f reflect.StructField) jsonInfo {
if !f.IsExported() {
return jsonInfo{omit: true}
}
info := jsonInfo{name: f.Name}
if tag, ok := f.Tag.Lookup("json"); ok {
name, rest, found := strings.Cut(tag, ",")
// "-" means omit, but "-," means the name is "-"
if name == "-" && !found {
return jsonInfo{omit: true}
}
if name != "" {
info.name = name
}
if len(rest) > 0 {
info.settings = map[string]bool{}
for _, s := range strings.Split(rest, ",") {
info.settings[s] = true
}
}
}
return info
}
// wrapf wraps *errp with the given formatted message if *errp is not nil.
func wrapf(errp *error, format string, args ...any) {
if *errp != nil {
*errp = fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), *errp)
}
}
+906
View File
@@ -0,0 +1,906 @@
// 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 jsonschema
import (
"encoding/json"
"errors"
"fmt"
"hash/maphash"
"iter"
"math"
"math/big"
"reflect"
"slices"
"strings"
"sync"
"unicode/utf8"
)
// The values of the "$schema" keyword for the versions that we can validate.
const (
draft7SchemaVersion = "http://json-schema.org/draft-07/schema#"
draft7SecSchemaVersion = "https://json-schema.org/draft-07/schema#"
draft202012SchemaVersion = "https://json-schema.org/draft/2020-12/schema"
)
// isValidSchemaVersion checks if the given schema version is supported
func isValidSchemaVersion(version string) bool {
return version == "" || version == draft7SchemaVersion || version == draft7SecSchemaVersion || version == draft202012SchemaVersion
}
// Validate validates the instance, which must be a JSON value, against the schema.
// It returns nil if validation is successful or an error if it is not.
// If the schema type is "object", instance should be a map[string]any.
func (rs *Resolved) Validate(instance any) error {
if s := rs.root.Schema; !isValidSchemaVersion(s) {
return fmt.Errorf("cannot validate version %s, supported versions: draft-07 and draft 2020-12", s)
}
st := &state{rs: rs}
return st.validate(reflect.ValueOf(instance), st.rs.root, nil)
}
// validateDefaults walks the schema tree. If it finds a default, it validates it
// against the schema containing it.
//
// TODO(jba): account for dynamic refs. This algorithm simple-mindedly
// treats each schema with a default as its own root.
func (rs *Resolved) validateDefaults() error {
if s := rs.root.Schema; !isValidSchemaVersion(s) {
return fmt.Errorf("cannot validate version %s, supported versions: draft-07 and draft 2020-12", s)
}
st := &state{rs: rs}
for s := range rs.root.all() {
// We checked for nil schemas in [Schema.Resolve].
assert(s != nil, "nil schema")
if s.DynamicRef != "" {
return fmt.Errorf("jsonschema: %s: validateDefaults does not support dynamic refs", rs.schemaString(s))
}
if s.Default != nil {
var d any
if err := json.Unmarshal(s.Default, &d); err != nil {
return fmt.Errorf("unmarshaling default value of schema %s: %w", rs.schemaString(s), err)
}
if err := st.validate(reflect.ValueOf(d), s, nil); err != nil {
return err
}
}
}
return nil
}
// state is the state of single call to ResolvedSchema.Validate.
type state struct {
rs *Resolved
// stack holds the schemas from recursive calls to validate.
// These are the "dynamic scopes" used to resolve dynamic references.
// https://json-schema.org/draft/2020-12/json-schema-core#scopes
stack []*Schema
}
// validate validates the reflected value of the instance.
func (st *state) validate(instance reflect.Value, schema *Schema, callerAnns *annotations) (err error) {
defer wrapf(&err, "validating %s", st.rs.schemaString(schema))
// Maintain a stack for dynamic schema resolution.
st.stack = append(st.stack, schema) // push
defer func() {
st.stack = st.stack[:len(st.stack)-1] // pop
}()
// We checked for nil schemas in [Schema.Resolve].
assert(schema != nil, "nil schema")
// Step through interfaces and pointers.
for instance.Kind() == reflect.Pointer || instance.Kind() == reflect.Interface {
instance = instance.Elem()
}
schemaInfo := st.rs.resolvedInfos[schema]
var anns annotations // all the annotations for this call and child calls
// $ref: https://json-schema.org/draft/2020-12/json-schema-core#section-8.2.3.1
if schema.Ref != "" {
if err := st.validate(instance, schemaInfo.resolvedRef, &anns); err != nil {
return err
}
// https://json-schema.org/draft-07/draft-handrews-json-schema-01#rfc.section.8.3
// "All other properties in a "$ref" object MUST be ignored."
if st.rs.draft == draft7 {
return nil
}
}
// type: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.1.1
if schema.Type != "" || schema.Types != nil {
gotType, ok := jsonType(instance)
if !ok {
return fmt.Errorf("type: %v of type %[1]T is not a valid JSON value", instance)
}
if schema.Type != "" {
// "number" subsumes integers
if !(gotType == schema.Type ||
gotType == "integer" && schema.Type == "number") {
return fmt.Errorf("type: %v has type %q, want %q", instance, gotType, schema.Type)
}
} else {
if !(slices.Contains(schema.Types, gotType) || (gotType == "integer" && slices.Contains(schema.Types, "number"))) {
return fmt.Errorf("type: %v has type %q, want one of %q",
instance, gotType, strings.Join(schema.Types, ", "))
}
}
}
// enum: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.1.2
if schema.Enum != nil {
ok := false
for _, e := range schema.Enum {
if equalValue(reflect.ValueOf(e), instance) {
ok = true
break
}
}
if !ok {
return fmt.Errorf("enum: %v does not equal any of: %v", instance, schema.Enum)
}
}
// const: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.1.3
if schema.Const != nil {
if !equalValue(reflect.ValueOf(*schema.Const), instance) {
return fmt.Errorf("const: %v does not equal %v", instance, *schema.Const)
}
}
// numbers: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.2
if schema.MultipleOf != nil || schema.Minimum != nil || schema.Maximum != nil || schema.ExclusiveMinimum != nil || schema.ExclusiveMaximum != nil {
n, ok := jsonNumber(instance)
if ok { // these keywords don't apply to non-numbers
if schema.MultipleOf != nil {
// TODO: validate MultipleOf as non-zero.
// The test suite assumes floats.
nf, _ := n.Float64() // don't care if it's exact or not
if _, f := math.Modf(nf / *schema.MultipleOf); f != 0 {
return fmt.Errorf("multipleOf: %s is not a multiple of %f", n, *schema.MultipleOf)
}
}
m := new(big.Rat) // reuse for all of the following
cmp := func(f float64) int { return n.Cmp(m.SetFloat64(f)) }
if schema.Minimum != nil && cmp(*schema.Minimum) < 0 {
return fmt.Errorf("minimum: %s is less than %f", n, *schema.Minimum)
}
if schema.Maximum != nil && cmp(*schema.Maximum) > 0 {
return fmt.Errorf("maximum: %s is greater than %f", n, *schema.Maximum)
}
if schema.ExclusiveMinimum != nil && cmp(*schema.ExclusiveMinimum) <= 0 {
return fmt.Errorf("exclusiveMinimum: %s is less than or equal to %f", n, *schema.ExclusiveMinimum)
}
if schema.ExclusiveMaximum != nil && cmp(*schema.ExclusiveMaximum) >= 0 {
return fmt.Errorf("exclusiveMaximum: %s is greater than or equal to %f", n, *schema.ExclusiveMaximum)
}
}
}
// strings: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.3
if instance.Kind() == reflect.String && (schema.MinLength != nil || schema.MaxLength != nil || schema.Pattern != "") {
str := instance.String()
n := utf8.RuneCountInString(str)
if schema.MinLength != nil {
if m := *schema.MinLength; n < m {
return fmt.Errorf("minLength: %q contains %d Unicode code points, fewer than %d", str, n, m)
}
}
if schema.MaxLength != nil {
if m := *schema.MaxLength; n > m {
return fmt.Errorf("maxLength: %q contains %d Unicode code points, more than %d", str, n, m)
}
}
if schema.Pattern != "" && !schemaInfo.pattern.MatchString(str) {
return fmt.Errorf("pattern: %q does not match regular expression %q", str, schema.Pattern)
}
}
// $dynamicRef: https://json-schema.org/draft/2020-12/json-schema-core#section-8.2.3.2
if schema.DynamicRef != "" {
// The ref behaves lexically or dynamically, but not both.
assert((schemaInfo.resolvedDynamicRef == nil) != (schemaInfo.dynamicRefAnchor == ""),
"DynamicRef not resolved properly")
if schemaInfo.resolvedDynamicRef != nil {
// Same as $ref.
if err := st.validate(instance, schemaInfo.resolvedDynamicRef, &anns); err != nil {
return err
}
} else {
// Dynamic behavior.
// Look for the base of the outermost schema on the stack with this dynamic
// anchor. (Yes, outermost: the one farthest from here. This the opposite
// of how ordinary dynamic variables behave.)
// Why the base of the schema being validated and not the schema itself?
// Because the base is the scope for anchors. In fact it's possible to
// refer to a schema that is not on the stack, but a child of some base
// on the stack.
// For an example, search for "detached" in testdata/draft2020-12/dynamicRef.json.
var dynamicSchema *Schema
for _, s := range st.stack {
base := st.rs.resolvedInfos[s].base
info, ok := st.rs.resolvedInfos[base].anchors[schemaInfo.dynamicRefAnchor]
if ok && info.dynamic {
dynamicSchema = info.schema
break
}
}
if dynamicSchema == nil {
return fmt.Errorf("missing dynamic anchor %q", schemaInfo.dynamicRefAnchor)
}
if err := st.validate(instance, dynamicSchema, &anns); err != nil {
return err
}
}
}
// logic
// https://json-schema.org/draft/2020-12/json-schema-core#section-10.2
// These must happen before arrays and objects because if they evaluate an item or property,
// then the unevaluatedItems/Properties schemas don't apply to it.
// See https://json-schema.org/draft/2020-12/json-schema-core#section-11.2, paragraph 4.
//
// If any of these fail, then validation fails, even if there is an unevaluatedXXX
// keyword in the schema. The spec is unclear about this, but that is the intention.
valid := func(s *Schema, anns *annotations) bool { return st.validate(instance, s, anns) == nil }
if schema.AllOf != nil {
for _, ss := range schema.AllOf {
if err := st.validate(instance, ss, &anns); err != nil {
return err
}
}
}
if schema.AnyOf != nil {
// We must visit them all, to collect annotations.
var errs []error
for _, ss := range schema.AnyOf {
if err := st.validate(instance, ss, &anns); err != nil {
errs = append(errs, err)
}
}
if len(errs) == len(schema.AnyOf) {
return fmt.Errorf("anyOf: did not validate against any of %v:\n%v",
schema.AnyOf, errors.Join(errs...))
}
}
if schema.OneOf != nil {
// Exactly one.
var okSchema *Schema
for _, ss := range schema.OneOf {
if valid(ss, &anns) {
if okSchema != nil {
return fmt.Errorf("oneOf: validated against both %v and %v", okSchema, ss)
}
okSchema = ss
}
}
if okSchema == nil {
return fmt.Errorf("oneOf: did not validate against any of %v", schema.OneOf)
}
}
if schema.Not != nil {
// Ignore annotations from "not".
if valid(schema.Not, nil) {
return fmt.Errorf("not: validated against %v", schema.Not)
}
}
if schema.If != nil {
var ss *Schema
if valid(schema.If, &anns) {
ss = schema.Then
} else {
ss = schema.Else
}
if ss != nil {
if err := st.validate(instance, ss, &anns); err != nil {
return err
}
}
}
// arrays
if instance.Kind() == reflect.Array || instance.Kind() == reflect.Slice {
// Handle both draft-07 and draft 2020-12
// https://json-schema.org/draft/2020-12/json-schema-core#section-10.3.1
// This validate call doesn't collect annotations for the items of the instance; they are separate
// instances in their own right.
// TODO(jba): if the test suite doesn't cover this case, add a test. For example, nested arrays.
if st.rs.draft == draft7 {
// For draft-07: additionalItems applies to remaining items after items array.
// If items is a Schema or if items is not set, additionalItems should be ignored
if schema.ItemsArray != nil {
for i, ischema := range schema.ItemsArray {
if i >= instance.Len() {
break // shorter is OK
}
if err := st.validate(instance.Index(i), ischema, nil); err != nil {
return err
}
}
anns.noteEndIndex(min(len(schema.ItemsArray), instance.Len()))
if schema.AdditionalItems != nil {
for i := len(schema.ItemsArray); i < instance.Len(); i++ {
if err := st.validate(instance.Index(i), schema.AdditionalItems, nil); err != nil {
return err
}
}
anns.allItems = true
}
} else if schema.Items != nil {
for i := 0; i < instance.Len(); i++ {
if err := st.validate(instance.Index(i), schema.Items, nil); err != nil {
return err
}
}
// Note that all the items in this array have been validated.
anns.allItems = true
}
} else if st.rs.draft == draft2020 {
// For draft 2020-12: items applies to remaining items after prefixItems
for i, ischema := range schema.PrefixItems {
if i >= instance.Len() {
break // shorter is OK
}
if err := st.validate(instance.Index(i), ischema, nil); err != nil {
return err
}
}
anns.noteEndIndex(min(len(schema.PrefixItems), instance.Len()))
if schema.Items != nil {
for i := len(schema.PrefixItems); i < instance.Len(); i++ {
if err := st.validate(instance.Index(i), schema.Items, nil); err != nil {
return err
}
}
// Note that all the items in this array have been validated.
anns.allItems = true
}
}
nContains := 0
if schema.Contains != nil {
for i := range instance.Len() {
if err := st.validate(instance.Index(i), schema.Contains, nil); err == nil {
nContains++
anns.noteIndex(i)
}
}
if nContains == 0 && (schema.MinContains == nil || *schema.MinContains > 0) {
return fmt.Errorf("contains: %s does not have an item matching %s", instance, schema.Contains)
}
}
// https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.4
// TODO(jba): check that these next four keywords' values are integers.
if schema.MinContains != nil && schema.Contains != nil {
if m := *schema.MinContains; nContains < m {
return fmt.Errorf("minContains: contains validated %d items, less than %d", nContains, m)
}
}
if schema.MaxContains != nil && schema.Contains != nil {
if m := *schema.MaxContains; nContains > m {
return fmt.Errorf("maxContains: contains validated %d items, greater than %d", nContains, m)
}
}
if schema.MinItems != nil {
if m := *schema.MinItems; instance.Len() < m {
return fmt.Errorf("minItems: array length %d is less than %d", instance.Len(), m)
}
}
if schema.MaxItems != nil {
if m := *schema.MaxItems; instance.Len() > m {
return fmt.Errorf("maxItems: array length %d is greater than %d", instance.Len(), m)
}
}
if schema.UniqueItems {
if instance.Len() > 1 {
// Hash each item and compare the hashes.
// If two hashes differ, the items differ.
// If two hashes are the same, compare the collisions for equality.
// (The same logic as hash table lookup.)
// TODO(jba): Use container/hash.Map when it becomes available (https://go.dev/issue/69559),
hashes := map[uint64][]int{} // from hash to indices
seed := maphash.MakeSeed()
for i := range instance.Len() {
item := instance.Index(i)
var h maphash.Hash
h.SetSeed(seed)
hashValue(&h, item)
hv := h.Sum64()
if sames := hashes[hv]; len(sames) > 0 {
for _, j := range sames {
if equalValue(item, instance.Index(j)) {
return fmt.Errorf("uniqueItems: array items %d and %d are equal", i, j)
}
}
}
hashes[hv] = append(hashes[hv], i)
}
}
}
// https://json-schema.org/draft/2020-12/json-schema-core#section-11.2
if schema.UnevaluatedItems != nil && !anns.allItems {
// Apply this subschema to all items in the array that haven't been successfully validated.
// That includes validations by subschemas on the same instance, like allOf.
for i := anns.endIndex; i < instance.Len(); i++ {
if !anns.evaluatedIndexes[i] {
if err := st.validate(instance.Index(i), schema.UnevaluatedItems, nil); err != nil {
return err
}
}
}
anns.allItems = true
}
}
// objects
// https://json-schema.org/draft/2020-12/json-schema-core#section-10.3.2
// Validating structs is problematic. See https://github.com/google/jsonschema-go/issues/23.
if instance.Kind() == reflect.Struct {
return errors.New("cannot validate against a struct; see https://github.com/google/jsonschema-go/issues/23 for details")
}
if instance.Kind() == reflect.Map {
if kt := instance.Type().Key(); kt.Kind() != reflect.String {
return fmt.Errorf("map key type %s is not a string", kt)
}
// Track the evaluated properties for just this schema, to support additionalProperties.
// If we used anns here, then we'd be including properties evaluated in subschemas
// from allOf, etc., which additionalProperties shouldn't observe.
evalProps := map[string]bool{}
for prop, subschema := range schema.Properties {
val := property(instance, prop)
if !val.IsValid() {
// It's OK if the instance doesn't have the property.
continue
}
// If the instance is a struct and an optional property has the zero
// value, then we could interpret it as present or missing. Be generous:
// assume it's missing, and thus always validates successfully.
if instance.Kind() == reflect.Struct && val.IsZero() && !schemaInfo.isRequired[prop] {
continue
}
if err := st.validate(val, subschema, nil); err != nil {
return err
}
evalProps[prop] = true
}
if len(schema.PatternProperties) > 0 {
for prop, val := range properties(instance) {
// Check every matching pattern.
for re, schema := range schemaInfo.patternProperties {
if re.MatchString(prop) {
if err := st.validate(val, schema, nil); err != nil {
return err
}
evalProps[prop] = true
}
}
}
}
if schema.AdditionalProperties != nil {
// Special case for a better error message when additional properties is
// 'falsy'
//
// If additionalProperties is {"not":{}} (which is how we
// unmarshal "false"), we can produce a better error message that
// summarizes all the extra properties. Otherwise, we fall back to the
// default validation.
//
// Note: this is much faster than comparing with falseSchema using Equal.
isFalsy := schema.AdditionalProperties.Not != nil && reflect.ValueOf(*schema.AdditionalProperties.Not).IsZero()
if isFalsy {
var disallowed []string
for prop := range properties(instance) {
if !evalProps[prop] {
disallowed = append(disallowed, prop)
}
}
if len(disallowed) > 0 {
return fmt.Errorf("unexpected additional properties %q", disallowed)
}
} else {
// Apply to all properties not handled above.
for prop, val := range properties(instance) {
if !evalProps[prop] {
if err := st.validate(val, schema.AdditionalProperties, nil); err != nil {
return err
}
evalProps[prop] = true
}
}
}
}
anns.noteProperties(evalProps)
if schema.PropertyNames != nil {
// Note: properties unnecessarily fetches each value. We could define a propertyNames function
// if performance ever matters.
for prop := range properties(instance) {
if err := st.validate(reflect.ValueOf(prop), schema.PropertyNames, nil); err != nil {
return err
}
}
}
// https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.5
var min, max int
if schema.MinProperties != nil || schema.MaxProperties != nil {
min, max = numPropertiesBounds(instance, schemaInfo.isRequired)
}
if schema.MinProperties != nil {
if n, m := max, *schema.MinProperties; n < m {
return fmt.Errorf("minProperties: object has %d properties, less than %d", n, m)
}
}
if schema.MaxProperties != nil {
if n, m := min, *schema.MaxProperties; n > m {
return fmt.Errorf("maxProperties: object has %d properties, greater than %d", n, m)
}
}
hasProperty := func(prop string) bool {
return property(instance, prop).IsValid()
}
missingProperties := func(props []string) []string {
var missing []string
for _, p := range props {
if !hasProperty(p) {
missing = append(missing, p)
}
}
return missing
}
if schema.Required != nil {
if m := missingProperties(schema.Required); len(m) > 0 {
return fmt.Errorf("required: missing properties: %q", m)
}
}
if st.rs.draft == draft7 {
if schema.DependencyStrings != nil {
for dprop, dstrings := range schema.DependencyStrings {
if hasProperty(dprop) {
if m := missingProperties(dstrings); len(m) > 0 {
return fmt.Errorf("dependentRequired[%q]: missing properties %q", dprop, m)
}
}
}
}
if schema.DependencySchemas != nil {
for dprop, dschema := range schema.DependencySchemas {
if hasProperty(dprop) {
err := st.validate(instance, dschema, &anns)
if err != nil {
return err
}
}
}
}
} else if st.rs.draft == draft2020 {
if schema.DependentRequired != nil {
// "Validation succeeds if, for each name that appears in both the instance
// and as a name within this keyword's value, every item in the corresponding
// array is also the name of a property in the instance." §6.5.4
for dprop, reqs := range schema.DependentRequired {
if hasProperty(dprop) {
if m := missingProperties(reqs); len(m) > 0 {
return fmt.Errorf("dependentRequired[%q]: missing properties %q", dprop, m)
}
}
}
}
// https://json-schema.org/draft/2020-12/json-schema-core#section-10.2.2.4
if schema.DependentSchemas != nil {
// This does not collect annotations, although it seems like it should.
for dprop, ss := range schema.DependentSchemas {
if hasProperty(dprop) {
// TODO: include dependentSchemas[dprop] in the errors.
err := st.validate(instance, ss, &anns)
if err != nil {
return err
}
}
}
}
}
if schema.UnevaluatedProperties != nil && !anns.allProperties {
// This looks a lot like AdditionalProperties, but depends on in-place keywords like allOf
// in addition to sibling keywords.
for prop, val := range properties(instance) {
if !anns.evaluatedProperties[prop] {
if err := st.validate(val, schema.UnevaluatedProperties, nil); err != nil {
return err
}
}
}
// The spec says the annotation should be the set of evaluated properties, but we can optimize
// by setting a single boolean, since after this succeeds all properties will be validated.
// See https://json-schema.slack.com/archives/CT7FF623C/p1745592564381459.
anns.allProperties = true
}
}
if callerAnns != nil {
// Our caller wants to know what we've validated.
callerAnns.merge(&anns)
}
return nil
}
// resolveDynamicRef returns the schema referred to by the argument schema's
// $dynamicRef value.
// It returns an error if the dynamic reference has no referent.
// If there is no $dynamicRef, resolveDynamicRef returns nil, nil.
// See https://json-schema.org/draft/2020-12/json-schema-core#section-8.2.3.2.
func (st *state) resolveDynamicRef(schema *Schema) (*Schema, error) {
if schema.DynamicRef == "" {
return nil, nil
}
info := st.rs.resolvedInfos[schema]
// The ref behaves lexically or dynamically, but not both.
assert((info.resolvedDynamicRef == nil) != (info.dynamicRefAnchor == ""),
"DynamicRef not statically resolved properly")
if r := info.resolvedDynamicRef; r != nil {
// Same as $ref.
return r, nil
}
// Dynamic behavior.
// Look for the base of the outermost schema on the stack with this dynamic
// anchor. (Yes, outermost: the one farthest from here. This the opposite
// of how ordinary dynamic variables behave.)
// Why the base of the schema being validated and not the schema itself?
// Because the base is the scope for anchors. In fact it's possible to
// refer to a schema that is not on the stack, but a child of some base
// on the stack.
// For an example, search for "detached" in testdata/draft2020-12/dynamicRef.json.
for _, s := range st.stack {
base := st.rs.resolvedInfos[s].base
info, ok := st.rs.resolvedInfos[base].anchors[info.dynamicRefAnchor]
if ok && info.dynamic {
return info.schema, nil
}
}
return nil, fmt.Errorf("missing dynamic anchor %q", info.dynamicRefAnchor)
}
// ApplyDefaults modifies an instance by applying the schema's defaults to it. If
// a schema or sub-schema has a default, then a corresponding missing instance value
// is set to the default.
//
// The JSON Schema specification does not describe how defaults should be interpreted.
// This method honors defaults only on properties, and only those that are not required.
// If the instance is a map and the property is missing, the property is added to
// the map with the default.
// ApplyDefaults does not support structs, because it cannot know whether a field
// is missing in the JSON, or was explicitly set to its zero value.
//
// ApplyDefaults can panic if a default cannot be assigned to a field.
//
// The argument must be a pointer to the instance.
// (In case we decide that top-level defaults are meaningful.)
//
// It is recommended to first call Resolve with a ValidateDefaults option of true,
// then call this method, and lastly call Validate.
func (rs *Resolved) ApplyDefaults(instancep any) error {
// TODO(jba): consider what defaults on top-level or array instances might mean.
// TODO(jba): follow $ref and $dynamicRef
st := &state{rs: rs}
return st.applyDefaults(reflect.ValueOf(instancep), rs.root)
}
// Recursive helper used by ApplyDefaults. Applies defaults on sub-schemas
// of object properties recursively.
func (st *state) applyDefaults(instancep reflect.Value, schema *Schema) (err error) {
defer wrapf(&err, "applyDefaults: schema %s, instance %v", st.rs.schemaString(schema), instancep)
schemaInfo := st.rs.resolvedInfos[schema]
instance := instancep.Elem()
if instance.Kind() == reflect.Interface && instance.IsValid() {
// If we unmarshalled into 'any', the default object unmarshalling will be map[string]any.
instance = instance.Elem()
}
if instance.Kind() == reflect.Map || instance.Kind() == reflect.Struct {
if instance.Kind() == reflect.Map {
if kt := instance.Type().Key(); kt.Kind() != reflect.String {
return fmt.Errorf("map key type %s is not a string", kt)
}
}
for prop, subschema := range schema.Properties {
// Ignore defaults on required properties. (A required property shouldn't have a default.)
if schemaInfo.isRequired[prop] {
continue
}
val := property(instance, prop)
switch instance.Kind() {
case reflect.Map:
// If there is a default for this property, and the map key is missing,
// set the map value to the default.
if subschema.Default != nil && !val.IsValid() {
// Create an lvalue, since map values aren't addressable.
lvalue := reflect.New(instance.Type().Elem())
if err := json.Unmarshal(subschema.Default, lvalue.Interface()); err != nil {
return err
}
// Recurse unconditionally; applyDefaults will only act on object-like values.
if err := st.applyDefaults(lvalue, subschema); err != nil {
return err
}
instance.SetMapIndex(reflect.ValueOf(prop), lvalue.Elem())
} else if val.IsValid() {
// Recurse into an existing sub-instance.
// MapIndex returns a non-addressable value; copy into an addressable lvalue, recurse, then set back.
lvalue := reflect.New(instance.Type().Elem())
// Initialize the lvalue with current value.
lvalue.Elem().Set(val)
if err := st.applyDefaults(lvalue, subschema); err != nil {
return err
}
instance.SetMapIndex(reflect.ValueOf(prop), lvalue.Elem())
} else if schemaHasDefaultsInProperties(subschema) {
// Property is missing, but descendants still have some defaults
// Create an empty container and recurse to populate
elemType := instance.Type().Elem()
var child reflect.Value
switch elemType.Kind() {
case reflect.Interface:
child = reflect.ValueOf(map[string]any{})
case reflect.Map:
child = reflect.MakeMap(elemType)
case reflect.Struct:
child = reflect.New(elemType).Elem()
}
if child.IsValid() {
lvalue := reflect.New(elemType)
lvalue.Elem().Set(child)
if err := st.applyDefaults(lvalue, subschema); err != nil {
return err
}
instance.SetMapIndex(reflect.ValueOf(prop), lvalue.Elem())
}
}
case reflect.Struct:
return errors.New("cannot apply defaults to a struct")
default:
panic(fmt.Sprintf("applyDefaults: property %s: bad value %s of kind %s",
prop, instance, instance.Kind()))
}
}
}
return nil
}
// schemaHasDefaultsInProperties reports whether s or any descendant schema under
// its Properties contains a default. Only walks Properties to match ApplyDefaults semantics.
func schemaHasDefaultsInProperties(s *Schema) bool {
if s == nil {
return false
}
if s.Default != nil {
return true
}
if s.Properties != nil {
for _, ss := range s.Properties {
if schemaHasDefaultsInProperties(ss) {
return true
}
}
}
return false
}
// property returns the value of the property of v with the given name, or the invalid
// reflect.Value if there is none.
// If v is a map, the property is the value of the map whose key is name.
// If v is a struct, the property is the value of the field with the given name according
// to the encoding/json package (see [jsonName]).
// If v is anything else, property panics.
func property(v reflect.Value, name string) reflect.Value {
switch v.Kind() {
case reflect.Map:
return v.MapIndex(reflect.ValueOf(name))
case reflect.Struct:
props := structPropertiesOf(v.Type())
// Ignore nonexistent properties.
if sf, ok := props[name]; ok {
return v.FieldByIndex(sf.Index)
}
return reflect.Value{}
default:
panic(fmt.Sprintf("property(%q): bad value %s of kind %s", name, v, v.Kind()))
}
}
// properties returns an iterator over the names and values of all properties
// in v, which must be a map or a struct.
// If a struct, zero-valued properties that are marked omitempty or omitzero
// are excluded.
func properties(v reflect.Value) iter.Seq2[string, reflect.Value] {
return func(yield func(string, reflect.Value) bool) {
switch v.Kind() {
case reflect.Map:
for k, e := range v.Seq2() {
if !yield(k.String(), e) {
return
}
}
case reflect.Struct:
for name, sf := range structPropertiesOf(v.Type()) {
val := v.FieldByIndex(sf.Index)
if val.IsZero() {
info := fieldJSONInfo(sf)
if info.settings["omitempty"] || info.settings["omitzero"] {
continue
}
}
if !yield(name, val) {
return
}
}
default:
panic(fmt.Sprintf("bad value %s of kind %s", v, v.Kind()))
}
}
}
// numPropertiesBounds returns bounds on the number of v's properties.
// v must be a map or a struct.
// If v is a map, both bounds are the map's size.
// If v is a struct, the max is the number of struct properties.
// But since we don't know whether a zero value indicates a missing optional property
// or not, be generous and use the number of non-zero properties as the min.
func numPropertiesBounds(v reflect.Value, isRequired map[string]bool) (int, int) {
switch v.Kind() {
case reflect.Map:
return v.Len(), v.Len()
case reflect.Struct:
sp := structPropertiesOf(v.Type())
min := 0
for prop, sf := range sp {
if !v.FieldByIndex(sf.Index).IsZero() || isRequired[prop] {
min++
}
}
return min, len(sp)
default:
panic(fmt.Sprintf("properties: bad value: %s of kind %s", v, v.Kind()))
}
}
// A propertyMap is a map from property name to struct field index.
type propertyMap = map[string]reflect.StructField
var structProperties sync.Map // from reflect.Type to propertyMap
// structPropertiesOf returns the JSON Schema properties for the struct type t.
// The caller must not mutate the result.
func structPropertiesOf(t reflect.Type) propertyMap {
// Mutex not necessary: at worst we'll recompute the same value.
if props, ok := structProperties.Load(t); ok {
return props.(propertyMap)
}
props := map[string]reflect.StructField{}
for _, sf := range reflect.VisibleFields(t) {
if sf.Anonymous {
continue
}
info := fieldJSONInfo(sf)
if !info.omit {
props[info.name] = sf
}
}
structProperties.Store(t, props)
return props
}
+41
View File
@@ -0,0 +1,41 @@
# Changelog
## [1.6.0](https://github.com/google/uuid/compare/v1.5.0...v1.6.0) (2024-01-16)
### Features
* add Max UUID constant ([#149](https://github.com/google/uuid/issues/149)) ([c58770e](https://github.com/google/uuid/commit/c58770eb495f55fe2ced6284f93c5158a62e53e3))
### Bug Fixes
* fix typo in version 7 uuid documentation ([#153](https://github.com/google/uuid/issues/153)) ([016b199](https://github.com/google/uuid/commit/016b199544692f745ffc8867b914129ecb47ef06))
* Monotonicity in UUIDv7 ([#150](https://github.com/google/uuid/issues/150)) ([a2b2b32](https://github.com/google/uuid/commit/a2b2b32373ff0b1a312b7fdf6d38a977099698a6))
## [1.5.0](https://github.com/google/uuid/compare/v1.4.0...v1.5.0) (2023-12-12)
### Features
* Validate UUID without creating new UUID ([#141](https://github.com/google/uuid/issues/141)) ([9ee7366](https://github.com/google/uuid/commit/9ee7366e66c9ad96bab89139418a713dc584ae29))
## [1.4.0](https://github.com/google/uuid/compare/v1.3.1...v1.4.0) (2023-10-26)
### Features
* UUIDs slice type with Strings() convenience method ([#133](https://github.com/google/uuid/issues/133)) ([cd5fbbd](https://github.com/google/uuid/commit/cd5fbbdd02f3e3467ac18940e07e062be1f864b4))
### Fixes
* Clarify that Parse's job is to parse but not necessarily validate strings. (Documents current behavior)
## [1.3.1](https://github.com/google/uuid/compare/v1.3.0...v1.3.1) (2023-08-18)
### Bug Fixes
* Use .EqualFold() to parse urn prefixed UUIDs ([#118](https://github.com/google/uuid/issues/118)) ([574e687](https://github.com/google/uuid/commit/574e6874943741fb99d41764c705173ada5293f0))
## Changelog
+26
View File
@@ -0,0 +1,26 @@
# How to contribute
We definitely welcome patches and contribution to this project!
### Tips
Commits must be formatted according to the [Conventional Commits Specification](https://www.conventionalcommits.org).
Always try to include a test case! If it is not possible or not necessary,
please explain why in the pull request description.
### Releasing
Commits that would precipitate a SemVer change, as described in the Conventional
Commits Specification, will trigger [`release-please`](https://github.com/google-github-actions/release-please-action)
to create a release candidate pull request. Once submitted, `release-please`
will create a release.
For tips on how to work with `release-please`, see its documentation.
### Legal requirements
In order to protect both you and ourselves, you will need to sign the
[Contributor License Agreement](https://cla.developers.google.com/clas).
You may have already signed it for other Google projects.
+9
View File
@@ -0,0 +1,9 @@
Paul Borman <borman@google.com>
bmatsuo
shawnps
theory
jboverfelt
dsymonds
cd1
wallclockbuilder
dansouza
+27
View File
@@ -0,0 +1,27 @@
Copyright (c) 2009,2014 Google Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+21
View File
@@ -0,0 +1,21 @@
# uuid
The uuid package generates and inspects UUIDs based on
[RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122)
and DCE 1.1: Authentication and Security Services.
This package is based on the github.com/pborman/uuid package (previously named
code.google.com/p/go-uuid). It differs from these earlier packages in that
a UUID is a 16 byte array rather than a byte slice. One loss due to this
change is the ability to represent an invalid UUID (vs a NIL UUID).
###### Install
```sh
go get github.com/google/uuid
```
###### Documentation
[![Go Reference](https://pkg.go.dev/badge/github.com/google/uuid.svg)](https://pkg.go.dev/github.com/google/uuid)
Full `go doc` style documentation for the package can be viewed online without
installing this package by using the GoDoc site here:
http://pkg.go.dev/github.com/google/uuid
+80
View File
@@ -0,0 +1,80 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"encoding/binary"
"fmt"
"os"
)
// A Domain represents a Version 2 domain
type Domain byte
// Domain constants for DCE Security (Version 2) UUIDs.
const (
Person = Domain(0)
Group = Domain(1)
Org = Domain(2)
)
// NewDCESecurity returns a DCE Security (Version 2) UUID.
//
// The domain should be one of Person, Group or Org.
// On a POSIX system the id should be the users UID for the Person
// domain and the users GID for the Group. The meaning of id for
// the domain Org or on non-POSIX systems is site defined.
//
// For a given domain/id pair the same token may be returned for up to
// 7 minutes and 10 seconds.
func NewDCESecurity(domain Domain, id uint32) (UUID, error) {
uuid, err := NewUUID()
if err == nil {
uuid[6] = (uuid[6] & 0x0f) | 0x20 // Version 2
uuid[9] = byte(domain)
binary.BigEndian.PutUint32(uuid[0:], id)
}
return uuid, err
}
// NewDCEPerson returns a DCE Security (Version 2) UUID in the person
// domain with the id returned by os.Getuid.
//
// NewDCESecurity(Person, uint32(os.Getuid()))
func NewDCEPerson() (UUID, error) {
return NewDCESecurity(Person, uint32(os.Getuid()))
}
// NewDCEGroup returns a DCE Security (Version 2) UUID in the group
// domain with the id returned by os.Getgid.
//
// NewDCESecurity(Group, uint32(os.Getgid()))
func NewDCEGroup() (UUID, error) {
return NewDCESecurity(Group, uint32(os.Getgid()))
}
// Domain returns the domain for a Version 2 UUID. Domains are only defined
// for Version 2 UUIDs.
func (uuid UUID) Domain() Domain {
return Domain(uuid[9])
}
// ID returns the id for a Version 2 UUID. IDs are only defined for Version 2
// UUIDs.
func (uuid UUID) ID() uint32 {
return binary.BigEndian.Uint32(uuid[0:4])
}
func (d Domain) String() string {
switch d {
case Person:
return "Person"
case Group:
return "Group"
case Org:
return "Org"
}
return fmt.Sprintf("Domain%d", int(d))
}
+12
View File
@@ -0,0 +1,12 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package uuid generates and inspects UUIDs.
//
// UUIDs are based on RFC 4122 and DCE 1.1: Authentication and Security
// Services.
//
// A UUID is a 16 byte (128 bit) array. UUIDs may be used as keys to
// maps or compared directly.
package uuid
+59
View File
@@ -0,0 +1,59 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"crypto/md5"
"crypto/sha1"
"hash"
)
// Well known namespace IDs and UUIDs
var (
NameSpaceDNS = Must(Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8"))
NameSpaceURL = Must(Parse("6ba7b811-9dad-11d1-80b4-00c04fd430c8"))
NameSpaceOID = Must(Parse("6ba7b812-9dad-11d1-80b4-00c04fd430c8"))
NameSpaceX500 = Must(Parse("6ba7b814-9dad-11d1-80b4-00c04fd430c8"))
Nil UUID // empty UUID, all zeros
// The Max UUID is special form of UUID that is specified to have all 128 bits set to 1.
Max = UUID{
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
}
)
// NewHash returns a new UUID derived from the hash of space concatenated with
// data generated by h. The hash should be at least 16 byte in length. The
// first 16 bytes of the hash are used to form the UUID. The version of the
// UUID will be the lower 4 bits of version. NewHash is used to implement
// NewMD5 and NewSHA1.
func NewHash(h hash.Hash, space UUID, data []byte, version int) UUID {
h.Reset()
h.Write(space[:]) //nolint:errcheck
h.Write(data) //nolint:errcheck
s := h.Sum(nil)
var uuid UUID
copy(uuid[:], s)
uuid[6] = (uuid[6] & 0x0f) | uint8((version&0xf)<<4)
uuid[8] = (uuid[8] & 0x3f) | 0x80 // RFC 4122 variant
return uuid
}
// NewMD5 returns a new MD5 (Version 3) UUID based on the
// supplied name space and data. It is the same as calling:
//
// NewHash(md5.New(), space, data, 3)
func NewMD5(space UUID, data []byte) UUID {
return NewHash(md5.New(), space, data, 3)
}
// NewSHA1 returns a new SHA1 (Version 5) UUID based on the
// supplied name space and data. It is the same as calling:
//
// NewHash(sha1.New(), space, data, 5)
func NewSHA1(space UUID, data []byte) UUID {
return NewHash(sha1.New(), space, data, 5)
}
+38
View File
@@ -0,0 +1,38 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import "fmt"
// MarshalText implements encoding.TextMarshaler.
func (uuid UUID) MarshalText() ([]byte, error) {
var js [36]byte
encodeHex(js[:], uuid)
return js[:], nil
}
// UnmarshalText implements encoding.TextUnmarshaler.
func (uuid *UUID) UnmarshalText(data []byte) error {
id, err := ParseBytes(data)
if err != nil {
return err
}
*uuid = id
return nil
}
// MarshalBinary implements encoding.BinaryMarshaler.
func (uuid UUID) MarshalBinary() ([]byte, error) {
return uuid[:], nil
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler.
func (uuid *UUID) UnmarshalBinary(data []byte) error {
if len(data) != 16 {
return fmt.Errorf("invalid UUID (got %d bytes)", len(data))
}
copy(uuid[:], data)
return nil
}
+90
View File
@@ -0,0 +1,90 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"sync"
)
var (
nodeMu sync.Mutex
ifname string // name of interface being used
nodeID [6]byte // hardware for version 1 UUIDs
zeroID [6]byte // nodeID with only 0's
)
// NodeInterface returns the name of the interface from which the NodeID was
// derived. The interface "user" is returned if the NodeID was set by
// SetNodeID.
func NodeInterface() string {
defer nodeMu.Unlock()
nodeMu.Lock()
return ifname
}
// SetNodeInterface selects the hardware address to be used for Version 1 UUIDs.
// If name is "" then the first usable interface found will be used or a random
// Node ID will be generated. If a named interface cannot be found then false
// is returned.
//
// SetNodeInterface never fails when name is "".
func SetNodeInterface(name string) bool {
defer nodeMu.Unlock()
nodeMu.Lock()
return setNodeInterface(name)
}
func setNodeInterface(name string) bool {
iname, addr := getHardwareInterface(name) // null implementation for js
if iname != "" && addr != nil {
ifname = iname
copy(nodeID[:], addr)
return true
}
// We found no interfaces with a valid hardware address. If name
// does not specify a specific interface generate a random Node ID
// (section 4.1.6)
if name == "" {
ifname = "random"
randomBits(nodeID[:])
return true
}
return false
}
// NodeID returns a slice of a copy of the current Node ID, setting the Node ID
// if not already set.
func NodeID() []byte {
defer nodeMu.Unlock()
nodeMu.Lock()
if nodeID == zeroID {
setNodeInterface("")
}
nid := nodeID
return nid[:]
}
// SetNodeID sets the Node ID to be used for Version 1 UUIDs. The first 6 bytes
// of id are used. If id is less than 6 bytes then false is returned and the
// Node ID is not set.
func SetNodeID(id []byte) bool {
if len(id) < 6 {
return false
}
defer nodeMu.Unlock()
nodeMu.Lock()
copy(nodeID[:], id)
ifname = "user"
return true
}
// NodeID returns the 6 byte node id encoded in uuid. It returns nil if uuid is
// not valid. The NodeID is only well defined for version 1 and 2 UUIDs.
func (uuid UUID) NodeID() []byte {
var node [6]byte
copy(node[:], uuid[10:])
return node[:]
}
+12
View File
@@ -0,0 +1,12 @@
// Copyright 2017 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build js
package uuid
// getHardwareInterface returns nil values for the JS version of the code.
// This removes the "net" dependency, because it is not used in the browser.
// Using the "net" library inflates the size of the transpiled JS code by 673k bytes.
func getHardwareInterface(name string) (string, []byte) { return "", nil }
+33
View File
@@ -0,0 +1,33 @@
// Copyright 2017 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !js
package uuid
import "net"
var interfaces []net.Interface // cached list of interfaces
// getHardwareInterface returns the name and hardware address of interface name.
// If name is "" then the name and hardware address of one of the system's
// interfaces is returned. If no interfaces are found (name does not exist or
// there are no interfaces) then "", nil is returned.
//
// Only addresses of at least 6 bytes are returned.
func getHardwareInterface(name string) (string, []byte) {
if interfaces == nil {
var err error
interfaces, err = net.Interfaces()
if err != nil {
return "", nil
}
}
for _, ifs := range interfaces {
if len(ifs.HardwareAddr) >= 6 && (name == "" || name == ifs.Name) {
return ifs.Name, ifs.HardwareAddr
}
}
return "", nil
}
+118
View File
@@ -0,0 +1,118 @@
// Copyright 2021 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"bytes"
"database/sql/driver"
"encoding/json"
"fmt"
)
var jsonNull = []byte("null")
// NullUUID represents a UUID that may be null.
// NullUUID implements the SQL driver.Scanner interface so
// it can be used as a scan destination:
//
// var u uuid.NullUUID
// err := db.QueryRow("SELECT name FROM foo WHERE id=?", id).Scan(&u)
// ...
// if u.Valid {
// // use u.UUID
// } else {
// // NULL value
// }
//
type NullUUID struct {
UUID UUID
Valid bool // Valid is true if UUID is not NULL
}
// Scan implements the SQL driver.Scanner interface.
func (nu *NullUUID) Scan(value interface{}) error {
if value == nil {
nu.UUID, nu.Valid = Nil, false
return nil
}
err := nu.UUID.Scan(value)
if err != nil {
nu.Valid = false
return err
}
nu.Valid = true
return nil
}
// Value implements the driver Valuer interface.
func (nu NullUUID) Value() (driver.Value, error) {
if !nu.Valid {
return nil, nil
}
// Delegate to UUID Value function
return nu.UUID.Value()
}
// MarshalBinary implements encoding.BinaryMarshaler.
func (nu NullUUID) MarshalBinary() ([]byte, error) {
if nu.Valid {
return nu.UUID[:], nil
}
return []byte(nil), nil
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler.
func (nu *NullUUID) UnmarshalBinary(data []byte) error {
if len(data) != 16 {
return fmt.Errorf("invalid UUID (got %d bytes)", len(data))
}
copy(nu.UUID[:], data)
nu.Valid = true
return nil
}
// MarshalText implements encoding.TextMarshaler.
func (nu NullUUID) MarshalText() ([]byte, error) {
if nu.Valid {
return nu.UUID.MarshalText()
}
return jsonNull, nil
}
// UnmarshalText implements encoding.TextUnmarshaler.
func (nu *NullUUID) UnmarshalText(data []byte) error {
id, err := ParseBytes(data)
if err != nil {
nu.Valid = false
return err
}
nu.UUID = id
nu.Valid = true
return nil
}
// MarshalJSON implements json.Marshaler.
func (nu NullUUID) MarshalJSON() ([]byte, error) {
if nu.Valid {
return json.Marshal(nu.UUID)
}
return jsonNull, nil
}
// UnmarshalJSON implements json.Unmarshaler.
func (nu *NullUUID) UnmarshalJSON(data []byte) error {
if bytes.Equal(data, jsonNull) {
*nu = NullUUID{}
return nil // valid null UUID
}
err := json.Unmarshal(data, &nu.UUID)
nu.Valid = err == nil
return err
}
+59
View File
@@ -0,0 +1,59 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"database/sql/driver"
"fmt"
)
// Scan implements sql.Scanner so UUIDs can be read from databases transparently.
// Currently, database types that map to string and []byte are supported. Please
// consult database-specific driver documentation for matching types.
func (uuid *UUID) Scan(src interface{}) error {
switch src := src.(type) {
case nil:
return nil
case string:
// if an empty UUID comes from a table, we return a null UUID
if src == "" {
return nil
}
// see Parse for required string format
u, err := Parse(src)
if err != nil {
return fmt.Errorf("Scan: %v", err)
}
*uuid = u
case []byte:
// if an empty UUID comes from a table, we return a null UUID
if len(src) == 0 {
return nil
}
// assumes a simple slice of bytes if 16 bytes
// otherwise attempts to parse
if len(src) != 16 {
return uuid.Scan(string(src))
}
copy((*uuid)[:], src)
default:
return fmt.Errorf("Scan: unable to scan type %T into UUID", src)
}
return nil
}
// Value implements sql.Valuer so that UUIDs can be written to databases
// transparently. Currently, UUIDs map to strings. Please consult
// database-specific driver documentation for matching types.
func (uuid UUID) Value() (driver.Value, error) {
return uuid.String(), nil
}
+134
View File
@@ -0,0 +1,134 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"encoding/binary"
"sync"
"time"
)
// A Time represents a time as the number of 100's of nanoseconds since 15 Oct
// 1582.
type Time int64
const (
lillian = 2299160 // Julian day of 15 Oct 1582
unix = 2440587 // Julian day of 1 Jan 1970
epoch = unix - lillian // Days between epochs
g1582 = epoch * 86400 // seconds between epochs
g1582ns100 = g1582 * 10000000 // 100s of a nanoseconds between epochs
)
var (
timeMu sync.Mutex
lasttime uint64 // last time we returned
clockSeq uint16 // clock sequence for this run
timeNow = time.Now // for testing
)
// UnixTime converts t the number of seconds and nanoseconds using the Unix
// epoch of 1 Jan 1970.
func (t Time) UnixTime() (sec, nsec int64) {
sec = int64(t - g1582ns100)
nsec = (sec % 10000000) * 100
sec /= 10000000
return sec, nsec
}
// GetTime returns the current Time (100s of nanoseconds since 15 Oct 1582) and
// clock sequence as well as adjusting the clock sequence as needed. An error
// is returned if the current time cannot be determined.
func GetTime() (Time, uint16, error) {
defer timeMu.Unlock()
timeMu.Lock()
return getTime()
}
func getTime() (Time, uint16, error) {
t := timeNow()
// If we don't have a clock sequence already, set one.
if clockSeq == 0 {
setClockSequence(-1)
}
now := uint64(t.UnixNano()/100) + g1582ns100
// If time has gone backwards with this clock sequence then we
// increment the clock sequence
if now <= lasttime {
clockSeq = ((clockSeq + 1) & 0x3fff) | 0x8000
}
lasttime = now
return Time(now), clockSeq, nil
}
// ClockSequence returns the current clock sequence, generating one if not
// already set. The clock sequence is only used for Version 1 UUIDs.
//
// The uuid package does not use global static storage for the clock sequence or
// the last time a UUID was generated. Unless SetClockSequence is used, a new
// random clock sequence is generated the first time a clock sequence is
// requested by ClockSequence, GetTime, or NewUUID. (section 4.2.1.1)
func ClockSequence() int {
defer timeMu.Unlock()
timeMu.Lock()
return clockSequence()
}
func clockSequence() int {
if clockSeq == 0 {
setClockSequence(-1)
}
return int(clockSeq & 0x3fff)
}
// SetClockSequence sets the clock sequence to the lower 14 bits of seq. Setting to
// -1 causes a new sequence to be generated.
func SetClockSequence(seq int) {
defer timeMu.Unlock()
timeMu.Lock()
setClockSequence(seq)
}
func setClockSequence(seq int) {
if seq == -1 {
var b [2]byte
randomBits(b[:]) // clock sequence
seq = int(b[0])<<8 | int(b[1])
}
oldSeq := clockSeq
clockSeq = uint16(seq&0x3fff) | 0x8000 // Set our variant
if oldSeq != clockSeq {
lasttime = 0
}
}
// Time returns the time in 100s of nanoseconds since 15 Oct 1582 encoded in
// uuid. The time is only defined for version 1, 2, 6 and 7 UUIDs.
func (uuid UUID) Time() Time {
var t Time
switch uuid.Version() {
case 6:
time := binary.BigEndian.Uint64(uuid[:8]) // Ignore uuid[6] version b0110
t = Time(time)
case 7:
time := binary.BigEndian.Uint64(uuid[:8])
t = Time((time>>16)*10000 + g1582ns100)
default: // forward compatible
time := int64(binary.BigEndian.Uint32(uuid[0:4]))
time |= int64(binary.BigEndian.Uint16(uuid[4:6])) << 32
time |= int64(binary.BigEndian.Uint16(uuid[6:8])&0xfff) << 48
t = Time(time)
}
return t
}
// ClockSequence returns the clock sequence encoded in uuid.
// The clock sequence is only well defined for version 1 and 2 UUIDs.
func (uuid UUID) ClockSequence() int {
return int(binary.BigEndian.Uint16(uuid[8:10])) & 0x3fff
}
+43
View File
@@ -0,0 +1,43 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"io"
)
// randomBits completely fills slice b with random data.
func randomBits(b []byte) {
if _, err := io.ReadFull(rander, b); err != nil {
panic(err.Error()) // rand should never fail
}
}
// xvalues returns the value of a byte as a hexadecimal digit or 255.
var xvalues = [256]byte{
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 255, 255, 255, 255, 255, 255,
255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
}
// xtob converts hex characters x1 and x2 into a byte.
func xtob(x1, x2 byte) (byte, bool) {
b1 := xvalues[x1]
b2 := xvalues[x2]
return (b1 << 4) | b2, b1 != 255 && b2 != 255
}
+365
View File
@@ -0,0 +1,365 @@
// Copyright 2018 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"bytes"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"io"
"strings"
"sync"
)
// A UUID is a 128 bit (16 byte) Universal Unique IDentifier as defined in RFC
// 4122.
type UUID [16]byte
// A Version represents a UUID's version.
type Version byte
// A Variant represents a UUID's variant.
type Variant byte
// Constants returned by Variant.
const (
Invalid = Variant(iota) // Invalid UUID
RFC4122 // The variant specified in RFC4122
Reserved // Reserved, NCS backward compatibility.
Microsoft // Reserved, Microsoft Corporation backward compatibility.
Future // Reserved for future definition.
)
const randPoolSize = 16 * 16
var (
rander = rand.Reader // random function
poolEnabled = false
poolMu sync.Mutex
poolPos = randPoolSize // protected with poolMu
pool [randPoolSize]byte // protected with poolMu
)
type invalidLengthError struct{ len int }
func (err invalidLengthError) Error() string {
return fmt.Sprintf("invalid UUID length: %d", err.len)
}
// IsInvalidLengthError is matcher function for custom error invalidLengthError
func IsInvalidLengthError(err error) bool {
_, ok := err.(invalidLengthError)
return ok
}
// Parse decodes s into a UUID or returns an error if it cannot be parsed. Both
// the standard UUID forms defined in RFC 4122
// (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx and
// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) are decoded. In addition,
// Parse accepts non-standard strings such as the raw hex encoding
// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx and 38 byte "Microsoft style" encodings,
// e.g. {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}. Only the middle 36 bytes are
// examined in the latter case. Parse should not be used to validate strings as
// it parses non-standard encodings as indicated above.
func Parse(s string) (UUID, error) {
var uuid UUID
switch len(s) {
// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
case 36:
// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
case 36 + 9:
if !strings.EqualFold(s[:9], "urn:uuid:") {
return uuid, fmt.Errorf("invalid urn prefix: %q", s[:9])
}
s = s[9:]
// {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}
case 36 + 2:
s = s[1:]
// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
case 32:
var ok bool
for i := range uuid {
uuid[i], ok = xtob(s[i*2], s[i*2+1])
if !ok {
return uuid, errors.New("invalid UUID format")
}
}
return uuid, nil
default:
return uuid, invalidLengthError{len(s)}
}
// s is now at least 36 bytes long
// it must be of the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' {
return uuid, errors.New("invalid UUID format")
}
for i, x := range [16]int{
0, 2, 4, 6,
9, 11,
14, 16,
19, 21,
24, 26, 28, 30, 32, 34,
} {
v, ok := xtob(s[x], s[x+1])
if !ok {
return uuid, errors.New("invalid UUID format")
}
uuid[i] = v
}
return uuid, nil
}
// ParseBytes is like Parse, except it parses a byte slice instead of a string.
func ParseBytes(b []byte) (UUID, error) {
var uuid UUID
switch len(b) {
case 36: // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
case 36 + 9: // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
if !bytes.EqualFold(b[:9], []byte("urn:uuid:")) {
return uuid, fmt.Errorf("invalid urn prefix: %q", b[:9])
}
b = b[9:]
case 36 + 2: // {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}
b = b[1:]
case 32: // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
var ok bool
for i := 0; i < 32; i += 2 {
uuid[i/2], ok = xtob(b[i], b[i+1])
if !ok {
return uuid, errors.New("invalid UUID format")
}
}
return uuid, nil
default:
return uuid, invalidLengthError{len(b)}
}
// s is now at least 36 bytes long
// it must be of the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
if b[8] != '-' || b[13] != '-' || b[18] != '-' || b[23] != '-' {
return uuid, errors.New("invalid UUID format")
}
for i, x := range [16]int{
0, 2, 4, 6,
9, 11,
14, 16,
19, 21,
24, 26, 28, 30, 32, 34,
} {
v, ok := xtob(b[x], b[x+1])
if !ok {
return uuid, errors.New("invalid UUID format")
}
uuid[i] = v
}
return uuid, nil
}
// MustParse is like Parse but panics if the string cannot be parsed.
// It simplifies safe initialization of global variables holding compiled UUIDs.
func MustParse(s string) UUID {
uuid, err := Parse(s)
if err != nil {
panic(`uuid: Parse(` + s + `): ` + err.Error())
}
return uuid
}
// FromBytes creates a new UUID from a byte slice. Returns an error if the slice
// does not have a length of 16. The bytes are copied from the slice.
func FromBytes(b []byte) (uuid UUID, err error) {
err = uuid.UnmarshalBinary(b)
return uuid, err
}
// Must returns uuid if err is nil and panics otherwise.
func Must(uuid UUID, err error) UUID {
if err != nil {
panic(err)
}
return uuid
}
// Validate returns an error if s is not a properly formatted UUID in one of the following formats:
// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
// {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}
// It returns an error if the format is invalid, otherwise nil.
func Validate(s string) error {
switch len(s) {
// Standard UUID format
case 36:
// UUID with "urn:uuid:" prefix
case 36 + 9:
if !strings.EqualFold(s[:9], "urn:uuid:") {
return fmt.Errorf("invalid urn prefix: %q", s[:9])
}
s = s[9:]
// UUID enclosed in braces
case 36 + 2:
if s[0] != '{' || s[len(s)-1] != '}' {
return fmt.Errorf("invalid bracketed UUID format")
}
s = s[1 : len(s)-1]
// UUID without hyphens
case 32:
for i := 0; i < len(s); i += 2 {
_, ok := xtob(s[i], s[i+1])
if !ok {
return errors.New("invalid UUID format")
}
}
default:
return invalidLengthError{len(s)}
}
// Check for standard UUID format
if len(s) == 36 {
if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' {
return errors.New("invalid UUID format")
}
for _, x := range []int{0, 2, 4, 6, 9, 11, 14, 16, 19, 21, 24, 26, 28, 30, 32, 34} {
if _, ok := xtob(s[x], s[x+1]); !ok {
return errors.New("invalid UUID format")
}
}
}
return nil
}
// String returns the string form of uuid, xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
// , or "" if uuid is invalid.
func (uuid UUID) String() string {
var buf [36]byte
encodeHex(buf[:], uuid)
return string(buf[:])
}
// URN returns the RFC 2141 URN form of uuid,
// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, or "" if uuid is invalid.
func (uuid UUID) URN() string {
var buf [36 + 9]byte
copy(buf[:], "urn:uuid:")
encodeHex(buf[9:], uuid)
return string(buf[:])
}
func encodeHex(dst []byte, uuid UUID) {
hex.Encode(dst, uuid[:4])
dst[8] = '-'
hex.Encode(dst[9:13], uuid[4:6])
dst[13] = '-'
hex.Encode(dst[14:18], uuid[6:8])
dst[18] = '-'
hex.Encode(dst[19:23], uuid[8:10])
dst[23] = '-'
hex.Encode(dst[24:], uuid[10:])
}
// Variant returns the variant encoded in uuid.
func (uuid UUID) Variant() Variant {
switch {
case (uuid[8] & 0xc0) == 0x80:
return RFC4122
case (uuid[8] & 0xe0) == 0xc0:
return Microsoft
case (uuid[8] & 0xe0) == 0xe0:
return Future
default:
return Reserved
}
}
// Version returns the version of uuid.
func (uuid UUID) Version() Version {
return Version(uuid[6] >> 4)
}
func (v Version) String() string {
if v > 15 {
return fmt.Sprintf("BAD_VERSION_%d", v)
}
return fmt.Sprintf("VERSION_%d", v)
}
func (v Variant) String() string {
switch v {
case RFC4122:
return "RFC4122"
case Reserved:
return "Reserved"
case Microsoft:
return "Microsoft"
case Future:
return "Future"
case Invalid:
return "Invalid"
}
return fmt.Sprintf("BadVariant%d", int(v))
}
// SetRand sets the random number generator to r, which implements io.Reader.
// If r.Read returns an error when the package requests random data then
// a panic will be issued.
//
// Calling SetRand with nil sets the random number generator to the default
// generator.
func SetRand(r io.Reader) {
if r == nil {
rander = rand.Reader
return
}
rander = r
}
// EnableRandPool enables internal randomness pool used for Random
// (Version 4) UUID generation. The pool contains random bytes read from
// the random number generator on demand in batches. Enabling the pool
// may improve the UUID generation throughput significantly.
//
// Since the pool is stored on the Go heap, this feature may be a bad fit
// for security sensitive applications.
//
// Both EnableRandPool and DisableRandPool are not thread-safe and should
// only be called when there is no possibility that New or any other
// UUID Version 4 generation function will be called concurrently.
func EnableRandPool() {
poolEnabled = true
}
// DisableRandPool disables the randomness pool if it was previously
// enabled with EnableRandPool.
//
// Both EnableRandPool and DisableRandPool are not thread-safe and should
// only be called when there is no possibility that New or any other
// UUID Version 4 generation function will be called concurrently.
func DisableRandPool() {
poolEnabled = false
defer poolMu.Unlock()
poolMu.Lock()
poolPos = randPoolSize
}
// UUIDs is a slice of UUID types.
type UUIDs []UUID
// Strings returns a string slice containing the string form of each UUID in uuids.
func (uuids UUIDs) Strings() []string {
var uuidStrs = make([]string, len(uuids))
for i, uuid := range uuids {
uuidStrs[i] = uuid.String()
}
return uuidStrs
}
+44
View File
@@ -0,0 +1,44 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"encoding/binary"
)
// NewUUID returns a Version 1 UUID based on the current NodeID and clock
// sequence, and the current time. If the NodeID has not been set by SetNodeID
// or SetNodeInterface then it will be set automatically. If the NodeID cannot
// be set NewUUID returns nil. If clock sequence has not been set by
// SetClockSequence then it will be set automatically. If GetTime fails to
// return the current NewUUID returns nil and an error.
//
// In most cases, New should be used.
func NewUUID() (UUID, error) {
var uuid UUID
now, seq, err := GetTime()
if err != nil {
return uuid, err
}
timeLow := uint32(now & 0xffffffff)
timeMid := uint16((now >> 32) & 0xffff)
timeHi := uint16((now >> 48) & 0x0fff)
timeHi |= 0x1000 // Version 1
binary.BigEndian.PutUint32(uuid[0:], timeLow)
binary.BigEndian.PutUint16(uuid[4:], timeMid)
binary.BigEndian.PutUint16(uuid[6:], timeHi)
binary.BigEndian.PutUint16(uuid[8:], seq)
nodeMu.Lock()
if nodeID == zeroID {
setNodeInterface("")
}
copy(uuid[10:], nodeID[:])
nodeMu.Unlock()
return uuid, nil
}
+76
View File
@@ -0,0 +1,76 @@
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import "io"
// New creates a new random UUID or panics. New is equivalent to
// the expression
//
// uuid.Must(uuid.NewRandom())
func New() UUID {
return Must(NewRandom())
}
// NewString creates a new random UUID and returns it as a string or panics.
// NewString is equivalent to the expression
//
// uuid.New().String()
func NewString() string {
return Must(NewRandom()).String()
}
// NewRandom returns a Random (Version 4) UUID.
//
// The strength of the UUIDs is based on the strength of the crypto/rand
// package.
//
// Uses the randomness pool if it was enabled with EnableRandPool.
//
// A note about uniqueness derived from the UUID Wikipedia entry:
//
// Randomly generated UUIDs have 122 random bits. One's annual risk of being
// hit by a meteorite is estimated to be one chance in 17 billion, that
// means the probability is about 0.00000000006 (6 × 1011),
// equivalent to the odds of creating a few tens of trillions of UUIDs in a
// year and having one duplicate.
func NewRandom() (UUID, error) {
if !poolEnabled {
return NewRandomFromReader(rander)
}
return newRandomFromPool()
}
// NewRandomFromReader returns a UUID based on bytes read from a given io.Reader.
func NewRandomFromReader(r io.Reader) (UUID, error) {
var uuid UUID
_, err := io.ReadFull(r, uuid[:])
if err != nil {
return Nil, err
}
uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4
uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10
return uuid, nil
}
func newRandomFromPool() (UUID, error) {
var uuid UUID
poolMu.Lock()
if poolPos == randPoolSize {
_, err := io.ReadFull(rander, pool[:])
if err != nil {
poolMu.Unlock()
return Nil, err
}
poolPos = 0
}
copy(uuid[:], pool[poolPos:(poolPos+16)])
poolPos += 16
poolMu.Unlock()
uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4
uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10
return uuid, nil
}
+56
View File
@@ -0,0 +1,56 @@
// Copyright 2023 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import "encoding/binary"
// UUID version 6 is a field-compatible version of UUIDv1, reordered for improved DB locality.
// It is expected that UUIDv6 will primarily be used in contexts where there are existing v1 UUIDs.
// Systems that do not involve legacy UUIDv1 SHOULD consider using UUIDv7 instead.
//
// see https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-03#uuidv6
//
// NewV6 returns a Version 6 UUID based on the current NodeID and clock
// sequence, and the current time. If the NodeID has not been set by SetNodeID
// or SetNodeInterface then it will be set automatically. If the NodeID cannot
// be set NewV6 set NodeID is random bits automatically . If clock sequence has not been set by
// SetClockSequence then it will be set automatically. If GetTime fails to
// return the current NewV6 returns Nil and an error.
func NewV6() (UUID, error) {
var uuid UUID
now, seq, err := GetTime()
if err != nil {
return uuid, err
}
/*
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| time_high |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| time_mid | time_low_and_version |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|clk_seq_hi_res | clk_seq_low | node (0-1) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| node (2-5) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
binary.BigEndian.PutUint64(uuid[0:], uint64(now))
binary.BigEndian.PutUint16(uuid[8:], seq)
uuid[6] = 0x60 | (uuid[6] & 0x0F)
uuid[8] = 0x80 | (uuid[8] & 0x3F)
nodeMu.Lock()
if nodeID == zeroID {
setNodeInterface("")
}
copy(uuid[10:], nodeID[:])
nodeMu.Unlock()
return uuid, nil
}
+104
View File
@@ -0,0 +1,104 @@
// Copyright 2023 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"io"
)
// UUID version 7 features a time-ordered value field derived from the widely
// implemented and well known Unix Epoch timestamp source,
// the number of milliseconds seconds since midnight 1 Jan 1970 UTC, leap seconds excluded.
// As well as improved entropy characteristics over versions 1 or 6.
//
// see https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-03#name-uuid-version-7
//
// Implementations SHOULD utilize UUID version 7 over UUID version 1 and 6 if possible.
//
// NewV7 returns a Version 7 UUID based on the current time(Unix Epoch).
// Uses the randomness pool if it was enabled with EnableRandPool.
// On error, NewV7 returns Nil and an error
func NewV7() (UUID, error) {
uuid, err := NewRandom()
if err != nil {
return uuid, err
}
makeV7(uuid[:])
return uuid, nil
}
// NewV7FromReader returns a Version 7 UUID based on the current time(Unix Epoch).
// it use NewRandomFromReader fill random bits.
// On error, NewV7FromReader returns Nil and an error.
func NewV7FromReader(r io.Reader) (UUID, error) {
uuid, err := NewRandomFromReader(r)
if err != nil {
return uuid, err
}
makeV7(uuid[:])
return uuid, nil
}
// makeV7 fill 48 bits time (uuid[0] - uuid[5]), set version b0111 (uuid[6])
// uuid[8] already has the right version number (Variant is 10)
// see function NewV7 and NewV7FromReader
func makeV7(uuid []byte) {
/*
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| unix_ts_ms |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| unix_ts_ms | ver | rand_a (12 bit seq) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|var| rand_b |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| rand_b |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
_ = uuid[15] // bounds check
t, s := getV7Time()
uuid[0] = byte(t >> 40)
uuid[1] = byte(t >> 32)
uuid[2] = byte(t >> 24)
uuid[3] = byte(t >> 16)
uuid[4] = byte(t >> 8)
uuid[5] = byte(t)
uuid[6] = 0x70 | (0x0F & byte(s>>8))
uuid[7] = byte(s)
}
// lastV7time is the last time we returned stored as:
//
// 52 bits of time in milliseconds since epoch
// 12 bits of (fractional nanoseconds) >> 8
var lastV7time int64
const nanoPerMilli = 1000000
// getV7Time returns the time in milliseconds and nanoseconds / 256.
// The returned (milli << 12 + seq) is guarenteed to be greater than
// (milli << 12 + seq) returned by any previous call to getV7Time.
func getV7Time() (milli, seq int64) {
timeMu.Lock()
defer timeMu.Unlock()
nano := timeNow().UnixNano()
milli = nano / nanoPerMilli
// Sequence number is between 0 and 3906 (nanoPerMilli>>8)
seq = (nano - milli*nanoPerMilli) >> 8
now := milli<<12 + seq
if now <= lastV7time {
now = lastV7time + 1
milli = now >> 12
seq = now & 0xfff
}
lastV7time = now
return milli, seq
}