// Copyright 2025 The JSON Schema Go Project Authors. All rights reserved.// Use of this source code is governed by an MIT-style// license that can be found in the LICENSE file.package jsonschemaimport ()// 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 supportedfunc isValidSchemaVersion( string) bool {return == "" || == draft7SchemaVersion || == draft7SecSchemaVersion || == 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 ( *Resolved) ( any) error {if := .root.Schema; !isValidSchemaVersion() {returnfmt.Errorf("cannot validate version %s, supported versions: draft-07 and draft 2020-12", ) } := &state{rs: }return .validate(reflect.ValueOf(), .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 ( *Resolved) () error {if := .root.Schema; !isValidSchemaVersion() {returnfmt.Errorf("cannot validate version %s, supported versions: draft-07 and draft 2020-12", ) } := &state{rs: }for := range .root.all() {// We checked for nil schemas in [Schema.Resolve].assert( != nil, "nil schema")if .DynamicRef != "" {returnfmt.Errorf("jsonschema: %s: validateDefaults does not support dynamic refs", .schemaString()) }if .Default != nil {varanyif := json.Unmarshal(.Default, &); != nil {returnfmt.Errorf("unmarshaling default value of schema %s: %w", .schemaString(), ) }if := .validate(reflect.ValueOf(), , nil); != nil {return } } }returnnil}// 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 ( *state) ( reflect.Value, *Schema, *annotations) ( error) {deferwrapf(&, "validating %s", .rs.schemaString())// Maintain a stack for dynamic schema resolution. .stack = append(.stack, ) // pushdeferfunc() { .stack = .stack[:len(.stack)-1] // pop }()// We checked for nil schemas in [Schema.Resolve].assert( != nil, "nil schema")// Step through interfaces and pointers.for .Kind() == reflect.Pointer || .Kind() == reflect.Interface { = .Elem() } := .rs.resolvedInfos[]varannotations// all the annotations for this call and child calls// $ref: https://json-schema.org/draft/2020-12/json-schema-core#section-8.2.3.1if .Ref != "" {if := .(, .resolvedRef, &); != nil {return }// 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 .rs.draft == draft7 {returnnil } }// type: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.1.1if .Type != "" || .Types != nil { , := jsonType()if ! {returnfmt.Errorf("type: %v of type %[1]T is not a valid JSON value", ) }if .Type != "" {// "number" subsumes integersif !( == .Type || == "integer" && .Type == "number") {returnfmt.Errorf("type: %v has type %q, want %q", , , .Type) } } else {if !(slices.Contains(.Types, ) || ( == "integer" && slices.Contains(.Types, "number"))) {returnfmt.Errorf("type: %v has type %q, want one of %q", , , strings.Join(.Types, ", ")) } } }// enum: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.1.2if .Enum != nil { := falsefor , := range .Enum {ifequalValue(reflect.ValueOf(), ) { = truebreak } }if ! {returnfmt.Errorf("enum: %v does not equal any of: %v", , .Enum) } }// const: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.1.3if .Const != nil {if !equalValue(reflect.ValueOf(*.Const), ) {returnfmt.Errorf("const: %v does not equal %v", , *.Const) } }// numbers: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.2if .MultipleOf != nil || .Minimum != nil || .Maximum != nil || .ExclusiveMinimum != nil || .ExclusiveMaximum != nil { , := jsonNumber()if { // these keywords don't apply to non-numbersif .MultipleOf != nil {// TODO: validate MultipleOf as non-zero. // The test suite assumes floats. , := .Float64() // don't care if it's exact or notif , := math.Modf( / *.MultipleOf); != 0 {returnfmt.Errorf("multipleOf: %s is not a multiple of %f", , *.MultipleOf) } } := new(big.Rat) // reuse for all of the following := func( float64) int { return .Cmp(.SetFloat64()) }if .Minimum != nil && (*.Minimum) < 0 {returnfmt.Errorf("minimum: %s is less than %f", , *.Minimum) }if .Maximum != nil && (*.Maximum) > 0 {returnfmt.Errorf("maximum: %s is greater than %f", , *.Maximum) }if .ExclusiveMinimum != nil && (*.ExclusiveMinimum) <= 0 {returnfmt.Errorf("exclusiveMinimum: %s is less than or equal to %f", , *.ExclusiveMinimum) }if .ExclusiveMaximum != nil && (*.ExclusiveMaximum) >= 0 {returnfmt.Errorf("exclusiveMaximum: %s is greater than or equal to %f", , *.ExclusiveMaximum) } } }// strings: https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.3if .Kind() == reflect.String && (.MinLength != nil || .MaxLength != nil || .Pattern != "") { := .String() := utf8.RuneCountInString()if .MinLength != nil {if := *.MinLength; < {returnfmt.Errorf("minLength: %q contains %d Unicode code points, fewer than %d", , , ) } }if .MaxLength != nil {if := *.MaxLength; > {returnfmt.Errorf("maxLength: %q contains %d Unicode code points, more than %d", , , ) } }if .Pattern != "" && !.pattern.MatchString() {returnfmt.Errorf("pattern: %q does not match regular expression %q", , .Pattern) } }// $dynamicRef: https://json-schema.org/draft/2020-12/json-schema-core#section-8.2.3.2if .DynamicRef != "" {// The ref behaves lexically or dynamically, but not both.assert((.resolvedDynamicRef == nil) != (.dynamicRefAnchor == ""),"DynamicRef not resolved properly")if .resolvedDynamicRef != nil {// Same as $ref.if := .(, .resolvedDynamicRef, &); != nil {return } } 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 *Schemafor , := range .stack { := .rs.resolvedInfos[].base , := .rs.resolvedInfos[].anchors[.dynamicRefAnchor]if && .dynamic { = .schemabreak } }if == nil {returnfmt.Errorf("missing dynamic anchor %q", .dynamicRefAnchor) }if := .(, , &); != nil {return } } }// 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. := func( *Schema, *annotations) bool { return .(, , ) == nil }if .AllOf != nil {for , := range .AllOf {if := .(, , &); != nil {return } } }if .AnyOf != nil {// We must visit them all, to collect annotations. := falsefor , := range .AnyOf {if (, &) { = true } }if ! {returnfmt.Errorf("anyOf: did not validate against any of %v", .AnyOf) } }if .OneOf != nil {// Exactly one.var *Schemafor , := range .OneOf {if (, &) {if != nil {returnfmt.Errorf("oneOf: validated against both %v and %v", , ) } = } }if == nil {returnfmt.Errorf("oneOf: did not validate against any of %v", .OneOf) } }if .Not != nil {// Ignore annotations from "not".if (.Not, nil) {returnfmt.Errorf("not: validated against %v", .Not) } }if .If != nil {var *Schemaif (.If, &) { = .Then } else { = .Else }if != nil {if := .(, , &); != nil {return } } }// arraysif .Kind() == reflect.Array || .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 .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 ignoredif .ItemsArray != nil {for , := range .ItemsArray {if >= .Len() {break// shorter is OK }if := .(.Index(), , nil); != nil {return } } .noteEndIndex(min(len(.ItemsArray), .Len()))if .AdditionalItems != nil {for := len(.ItemsArray); < .Len(); ++ {if := .(.Index(), .AdditionalItems, nil); != nil {return } } .allItems = true } } elseif .Items != nil {for := 0; < .Len(); ++ {if := .(.Index(), .Items, nil); != nil {return } }// Note that all the items in this array have been validated. .allItems = true } } elseif .rs.draft == draft2020 {// For draft 2020-12: items applies to remaining items after prefixItemsfor , := range .PrefixItems {if >= .Len() {break// shorter is OK }if := .(.Index(), , nil); != nil {return } } .noteEndIndex(min(len(.PrefixItems), .Len()))if .Items != nil {for := len(.PrefixItems); < .Len(); ++ {if := .(.Index(), .Items, nil); != nil {return } }// Note that all the items in this array have been validated. .allItems = true } } := 0if .Contains != nil {for := range .Len() {if := .(.Index(), .Contains, nil); == nil { ++ .noteIndex() } }if == 0 && (.MinContains == nil || *.MinContains > 0) {returnfmt.Errorf("contains: %s does not have an item matching %s", , .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 .MinContains != nil && .Contains != nil {if := *.MinContains; < {returnfmt.Errorf("minContains: contains validated %d items, less than %d", , ) } }if .MaxContains != nil && .Contains != nil {if := *.MaxContains; > {returnfmt.Errorf("maxContains: contains validated %d items, greater than %d", , ) } }if .MinItems != nil {if := *.MinItems; .Len() < {returnfmt.Errorf("minItems: array length %d is less than %d", .Len(), ) } }if .MaxItems != nil {if := *.MaxItems; .Len() > {returnfmt.Errorf("maxItems: array length %d is greater than %d", .Len(), ) } }if .UniqueItems {if .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), := map[uint64][]int{} // from hash to indices := maphash.MakeSeed()for := range .Len() { := .Index()varmaphash.Hash .SetSeed()hashValue(&, ) := .Sum64()if := []; len() > 0 {for , := range {ifequalValue(, .Index()) {returnfmt.Errorf("uniqueItems: array items %d and %d are equal", , ) } } } [] = append([], ) } } }// https://json-schema.org/draft/2020-12/json-schema-core#section-11.2if .UnevaluatedItems != nil && !.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 := .endIndex; < .Len(); ++ {if !.evaluatedIndexes[] {if := .(.Index(), .UnevaluatedItems, nil); != nil {return } } } .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 .Kind() == reflect.Struct {returnerrors.New("cannot validate against a struct; see https://github.com/google/jsonschema-go/issues/23 for details") }if .Kind() == reflect.Map {if := .Type().Key(); .Kind() != reflect.String {returnfmt.Errorf("map key type %s is not a string", ) }// 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. := map[string]bool{}for , := range .Properties { := property(, )if !.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 .Kind() == reflect.Struct && .IsZero() && !.isRequired[] {continue }if := .(, , nil); != nil {return } [] = true }iflen(.PatternProperties) > 0 {for , := rangeproperties() {// Check every matching pattern.for , := range .patternProperties {if .MatchString() {if := .(, , nil); != nil {return } [] = true } } } }if .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. := .AdditionalProperties.Not != nil && reflect.ValueOf(*.AdditionalProperties.Not).IsZero()if {var []stringfor := rangeproperties() {if ![] { = append(, ) } }iflen() > 0 {returnfmt.Errorf("unexpected additional properties %q", ) } } else {// Apply to all properties not handled above.for , := rangeproperties() {if ![] {if := .(, .AdditionalProperties, nil); != nil {return } [] = true } } } } .noteProperties()if .PropertyNames != nil {// Note: properties unnecessarily fetches each value. We could define a propertyNames function // if performance ever matters.for := rangeproperties() {if := .(reflect.ValueOf(), .PropertyNames, nil); != nil {return } } }// https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-01#section-6.5var , intif .MinProperties != nil || .MaxProperties != nil { , = numPropertiesBounds(, .isRequired) }if .MinProperties != nil {if , := , *.MinProperties; < {returnfmt.Errorf("minProperties: object has %d properties, less than %d", , ) } }if .MaxProperties != nil {if , := , *.MaxProperties; > {returnfmt.Errorf("maxProperties: object has %d properties, greater than %d", , ) } } := func( string) bool {returnproperty(, ).IsValid() } := func( []string) []string {var []stringfor , := range {if !() { = append(, ) } }return }if .Required != nil {if := (.Required); len() > 0 {returnfmt.Errorf("required: missing properties: %q", ) } }if .rs.draft == draft7 {if .DependencyStrings != nil {for , := range .DependencyStrings {if () {if := (); len() > 0 {returnfmt.Errorf("dependentRequired[%q]: missing properties %q", , ) } } } }if .DependencySchemas != nil {for , := range .DependencySchemas {if () { := .(, , &)if != nil {return } } } } } elseif .rs.draft == draft2020 {if .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.4for , := range .DependentRequired {if () {if := (); len() > 0 {returnfmt.Errorf("dependentRequired[%q]: missing properties %q", , ) } } } }// https://json-schema.org/draft/2020-12/json-schema-core#section-10.2.2.4if .DependentSchemas != nil {// This does not collect annotations, although it seems like it should.for , := range .DependentSchemas {if () {// TODO: include dependentSchemas[dprop] in the errors. := .(, , &)if != nil {return } } } } }if .UnevaluatedProperties != nil && !.allProperties {// This looks a lot like AdditionalProperties, but depends on in-place keywords like allOf // in addition to sibling keywords.for , := rangeproperties() {if !.evaluatedProperties[] {if := .(, .UnevaluatedProperties, nil); != nil {return } } }// 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. .allProperties = true } }if != nil {// Our caller wants to know what we've validated. .merge(&) }returnnil}// 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 ( *state) ( *Schema) (*Schema, error) {if .DynamicRef == "" {returnnil, nil } := .rs.resolvedInfos[]// The ref behaves lexically or dynamically, but not both.assert((.resolvedDynamicRef == nil) != (.dynamicRefAnchor == ""),"DynamicRef not statically resolved properly")if := .resolvedDynamicRef; != nil {// Same as $ref.return , 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 , := range .stack { := .rs.resolvedInfos[].base , := .rs.resolvedInfos[].anchors[.dynamicRefAnchor]if && .dynamic {return .schema, nil } }returnnil, fmt.Errorf("missing dynamic anchor %q", .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 ( *Resolved) ( any) error {// TODO(jba): consider what defaults on top-level or array instances might mean. // TODO(jba): follow $ref and $dynamicRef := &state{rs: }return .applyDefaults(reflect.ValueOf(), .root)}// Recursive helper used by ApplyDefaults. Applies defaults on sub-schemas// of object properties recursively.func ( *state) ( reflect.Value, *Schema) ( error) {deferwrapf(&, "applyDefaults: schema %s, instance %v", .rs.schemaString(), ) := .rs.resolvedInfos[] := .Elem()if .Kind() == reflect.Interface && .IsValid() {// If we unmarshalled into 'any', the default object unmarshalling will be map[string]any. = .Elem() }if .Kind() == reflect.Map || .Kind() == reflect.Struct {if .Kind() == reflect.Map {if := .Type().Key(); .Kind() != reflect.String {returnfmt.Errorf("map key type %s is not a string", ) } }for , := range .Properties {// Ignore defaults on required properties. (A required property shouldn't have a default.)if .isRequired[] {continue } := property(, )switch .Kind() {casereflect.Map:// If there is a default for this property, and the map key is missing, // set the map value to the default.if .Default != nil && !.IsValid() {// Create an lvalue, since map values aren't addressable. := reflect.New(.Type().Elem())if := json.Unmarshal(.Default, .Interface()); != nil {return }// Recurse unconditionally; applyDefaults will only act on object-like values.if := .(, ); != nil {return } .SetMapIndex(reflect.ValueOf(), .Elem()) } elseif .IsValid() {// Recurse into an existing sub-instance. // MapIndex returns a non-addressable value; copy into an addressable lvalue, recurse, then set back. := reflect.New(.Type().Elem())// Initialize the lvalue with current value. .Elem().Set()if := .(, ); != nil {return } .SetMapIndex(reflect.ValueOf(), .Elem()) } elseifschemaHasDefaultsInProperties() {// Property is missing, but descendants still have some defaults // Create an empty container and recurse to populate := .Type().Elem()varreflect.Valueswitch .Kind() {casereflect.Interface: = reflect.ValueOf(map[string]any{})casereflect.Map: = reflect.MakeMap()casereflect.Struct: = reflect.New().Elem() }if .IsValid() { := reflect.New() .Elem().Set()if := .(, ); != nil {return } .SetMapIndex(reflect.ValueOf(), .Elem()) } }casereflect.Struct:returnerrors.New("cannot apply defaults to a struct")default:panic(fmt.Sprintf("applyDefaults: property %s: bad value %s of kind %s", , , .Kind())) } } }returnnil}// schemaHasDefaultsInProperties reports whether s or any descendant schema under// its Properties contains a default. Only walks Properties to match ApplyDefaults semantics.func schemaHasDefaultsInProperties( *Schema) bool {if == nil {returnfalse }if .Default != nil {returntrue }if .Properties != nil {for , := range .Properties {if () {returntrue } } }returnfalse}// 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( reflect.Value, string) reflect.Value {switch .Kind() {casereflect.Map:return .MapIndex(reflect.ValueOf())casereflect.Struct: := structPropertiesOf(.Type())// Ignore nonexistent properties.if , := []; {return .FieldByIndex(.Index) }returnreflect.Value{}default:panic(fmt.Sprintf("property(%q): bad value %s of kind %s", , , .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( reflect.Value) iter.Seq2[string, reflect.Value] {returnfunc( func(string, reflect.Value) bool) {switch .Kind() {casereflect.Map:for , := range .Seq2() {if !(.String(), ) {return } }casereflect.Struct:for , := rangestructPropertiesOf(.Type()) { := .FieldByIndex(.Index)if .IsZero() { := fieldJSONInfo()if .settings["omitempty"] || .settings["omitzero"] {continue } }if !(, ) {return } }default:panic(fmt.Sprintf("bad value %s of kind %s", , .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( reflect.Value, map[string]bool) (int, int) {switch .Kind() {casereflect.Map:return .Len(), .Len()casereflect.Struct: := structPropertiesOf(.Type()) := 0for , := range {if !.FieldByIndex(.Index).IsZero() || [] { ++ } }return , len()default:panic(fmt.Sprintf("properties: bad value: %s of kind %s", , .Kind())) }}// A propertyMap is a map from property name to struct field index.type propertyMap = map[string]reflect.StructFieldvar 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( reflect.Type) propertyMap {// Mutex not necessary: at worst we'll recompute the same value.if , := structProperties.Load(); {return .(propertyMap) } := map[string]reflect.StructField{}for , := rangereflect.VisibleFields() {if .Anonymous {continue } := fieldJSONInfo()if !.omit { [.name] = } }structProperties.Store(, )return}
The pages are generated with Goldsv0.8.4. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.