Skip to main content
Glama
jsonschema.go34.6 kB
// Copyright 2024-2025 Buf Technologies, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package buf // This is an adapted version import ( "fmt" "math" "slices" "strings" "unicode" "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" "github.com/bufbuild/protovalidate-go/resolve" "google.golang.org/protobuf/reflect/protoreflect" ) // An enumeration of the JSON Schema type names. const ( jsArray = "array" jsBoolean = "boolean" jsInteger = "integer" jsNull = "null" jsNumber = "number" jsObject = "object" jsString = "string" ) type FieldVisibility int const ( FieldVisible FieldVisibility = iota FieldHide FieldIgnore ) type GeneratorOption func(*jsonSchemaGenerator) // WithJSONNames sets the generator to use JSON field names as the primary name. func WithJSONNames() GeneratorOption { return func(p *jsonSchemaGenerator) { p.useJSONNames = true } } // Generate generates a JSON schema for the given message descriptor, with protobuf field names. func Generate(input protoreflect.MessageDescriptor, opts ...GeneratorOption) map[string]interface{} { generator := &jsonSchemaGenerator{} generator.custom = generator.makeWktGenerators() for _, opt := range opts { opt(generator) } schema := map[string]interface{}{} generator.generate(input, schema) return schema } type jsonSchemaGenerator struct { custom map[protoreflect.FullName]func(protoreflect.MessageDescriptor, *validate.FieldConstraints, map[string]interface{}) useJSONNames bool } func (p *jsonSchemaGenerator) generate(desc protoreflect.MessageDescriptor, schema map[string]interface{}) { if custom, ok := p.custom[desc.FullName()]; ok { // Custom generator. custom(desc, nil, schema) } else { // Default generator. p.generateDefault(desc, schema) } } func (p *jsonSchemaGenerator) generateDefault(desc protoreflect.MessageDescriptor, schema map[string]interface{}) { schema["type"] = jsObject p.setDescription(desc, schema) var required []string properties := make(map[string]interface{}) patternProperties := make(map[string]interface{}) for i := range desc.Fields().Len() { field := desc.Fields().Get(i) visibility := p.shouldIgnoreField(field) if visibility == FieldIgnore { continue } constraints := p.getFieldConstraints(field) if constraints.GetRequired() && constraints.GetIgnore() != validate.Ignore_IGNORE_IF_UNPOPULATED { required = append(required, string(field.Name())) } // Generate the schema. fieldSchema := p.generateField(field, constraints) // TODO: Add an option to include custom alias. aliases := make([]string, 0, 1) switch { case visibility == FieldHide: aliases = append(aliases, string(field.Name())) if field.JSONName() != string(field.Name()) { aliases = append(aliases, field.JSONName()) } case p.useJSONNames: // Use the JSON name as the primary name. properties[field.JSONName()] = fieldSchema if field.JSONName() != string(field.Name()) { aliases = append(aliases, string(field.Name())) } default: // Use the proto name as the primary name. properties[string(field.Name())] = fieldSchema if field.JSONName() != string(field.Name()) { aliases = append(aliases, field.JSONName()) } } if len(aliases) > 0 { pattern := "^(" + strings.Join(aliases, "|") + ")$" patternProperties[pattern] = fieldSchema } } schema["properties"] = properties schema["additionalProperties"] = false if len(patternProperties) > 0 { schema["patternProperties"] = patternProperties } if len(required) > 0 { schema["required"] = required } } func (p *jsonSchemaGenerator) setDescription(desc protoreflect.Descriptor, schema map[string]interface{}) { src := desc.ParentFile().SourceLocations().ByDescriptor(desc) var descriptions []string if src.LeadingComments != "" { descriptions = append(descriptions, strings.TrimSpace(src.LeadingComments)) } if src.TrailingComments != "" { descriptions = append(descriptions, strings.TrimSpace(src.TrailingComments)) } if len(descriptions) > 0 { schema["description"] = strings.Join(descriptions, " | ") } } func (p *jsonSchemaGenerator) generateField(field protoreflect.FieldDescriptor, constraints *validate.FieldConstraints) map[string]interface{} { var schema = make(map[string]interface{}) p.setDescription(field, schema) p.generateValidation(field, constraints, schema) return schema } func (p *jsonSchemaGenerator) generateValidation(field protoreflect.FieldDescriptor, constraints *validate.FieldConstraints, schema map[string]interface{}) { if field.IsList() { schema["type"] = jsArray items := make(map[string]interface{}) schema["items"] = items schema = items constraints = constraints.GetRepeated().GetItems() } switch field.Kind() { case protoreflect.BoolKind: p.generateBoolValidation(field, constraints, schema) case protoreflect.EnumKind: p.generateEnumValidation(field, constraints, schema) case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind: p.generateInt32Validation(field, constraints, schema) case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind: p.generateInt64Validation(field, constraints, schema) case protoreflect.Uint32Kind, protoreflect.Fixed32Kind: p.generateUint32Validation(field, constraints, schema) case protoreflect.Uint64Kind, protoreflect.Fixed64Kind: p.generateUint64Validation(field, constraints, schema) case protoreflect.FloatKind: p.generateFloatValidation(field, constraints, schema, 32) case protoreflect.DoubleKind: p.generateFloatValidation(field, constraints, schema, 64) case protoreflect.StringKind: p.generateStringValidation(field, constraints, schema) case protoreflect.BytesKind: p.generateBytesValidation(field, constraints, schema) case protoreflect.MessageKind, protoreflect.GroupKind: if field.IsMap() { schema["type"] = jsObject propertyNames := make(map[string]interface{}) constraints := p.getFieldConstraints(field) p.generateValidation(field.MapKey(), constraints.GetMap().GetKeys(), propertyNames) schema["propertyNames"] = propertyNames properties := make(map[string]interface{}) p.generateValidation(field.MapValue(), constraints.GetMap().GetValues(), properties) schema["additionalProperties"] = properties } else { p.generateMessageValidation(field, schema) } } } func (p *jsonSchemaGenerator) getFieldConstraints(field protoreflect.FieldDescriptor) *validate.FieldConstraints { constraints := resolve.FieldConstraints(field) if constraints == nil || constraints.GetIgnore() == validate.Ignore_IGNORE_ALWAYS { return nil } return constraints } func (p *jsonSchemaGenerator) generateBoolValidation(field protoreflect.FieldDescriptor, constraints *validate.FieldConstraints, schema map[string]interface{}) { schema["type"] = jsBoolean if !field.HasPresence() && constraints.GetRequired() && constraints.GetIgnore() != validate.Ignore_IGNORE_IF_DEFAULT_VALUE { // False is not allowed. schema["enum"] = []bool{true} } else if constraints.GetBool() != nil && constraints.GetBool().Const != nil { schema["enum"] = []bool{constraints.GetBool().GetConst()} } } func generateTitle(name protoreflect.Name) string { // Convert camel case to space separated words. var result strings.Builder for i, chr := range name { isUpper := unicode.IsUpper(chr) nextIsUpper := i+1 >= len(name) || unicode.IsUpper(rune(name[i+1])) if i > 0 && isUpper && !nextIsUpper { result.WriteRune(' ') } result.WriteRune(chr) } return result.String() } type enumFieldSelector struct { selected bool index int } func (p *jsonSchemaGenerator) generateEnumValidation(field protoreflect.FieldDescriptor, constraints *validate.FieldConstraints, schema map[string]interface{}) { enumFieldSelectors := make(map[int32]enumFieldSelector, field.Enum().Values().Len()) for i := range field.Enum().Values().Len() { val := field.Enum().Values().Get(i) enumFieldSelectors[int32(val.Number())] = enumFieldSelector{ selected: true, index: i, } } if constraints.GetEnum() != nil && constraints.GetEnum().HasConst() { for number := range enumFieldSelectors { if number != constraints.GetEnum().GetConst() { enumFieldSelectors[number] = enumFieldSelector{} } } } if constraints.GetEnum() != nil && len(constraints.GetEnum().In) > 0 { inMap := make(map[int32]struct{}, len(constraints.GetEnum().In)) for _, value := range constraints.GetEnum().In { inMap[value] = struct{}{} } for number := range enumFieldSelectors { if _, ok := inMap[number]; !ok { enumFieldSelectors[number] = enumFieldSelector{} } } } if constraints.GetEnum() != nil && len(constraints.GetEnum().NotIn) > 0 { for _, value := range constraints.GetEnum().NotIn { enumFieldSelectors[value] = enumFieldSelector{} } } onlySelectIntValues := constraints.GetEnum() != nil && (constraints.GetEnum().GetDefinedOnly() || constraints.GetEnum().HasConst() || constraints.GetEnum().GetIn() != nil) validIntegers := map[string]interface{}{"type": jsInteger, "minimum": math.MinInt32, "maximum": math.MaxInt32} if onlySelectIntValues { var integerValues = make([]int32, 0) for number, val := range enumFieldSelectors { if val.selected { integerValues = append(integerValues, number) } } slices.Sort(integerValues) validIntegers = map[string]interface{}{"type": jsInteger, "enum": integerValues} } validIndexes := make([]int, 0, len(enumFieldSelectors)) for _, val := range enumFieldSelectors { if val.selected { validIndexes = append(validIndexes, val.index) } } slices.Sort(validIndexes) var stringValues = make([]string, 0) for _, index := range validIndexes { stringValues = append(stringValues, string(field.Enum().Values().Get(index).Name())) } validStrings := map[string]interface{}{"type": jsString, "enum": stringValues, "title": generateTitle(field.Enum().Name())} schema["anyOf"] = []map[string]interface{}{validStrings, validIntegers} } type baseRule[T comparable] interface { HasConst() bool GetConst() T GetIn() []T } type numberRule[T comparable] interface { baseRule[T] HasGte() bool GetGte() T HasGt() bool GetGt() T HasLte() bool GetLte() T HasLt() bool GetLt() T } func generateConstInValidation[T comparable](constraints baseRule[T], schema map[string]interface{}) { if constraints.HasConst() { schema["enum"] = []T{constraints.GetConst()} } else if len(constraints.GetIn()) > 0 { schema["enum"] = constraints.GetIn() } } func generateIntValidation[T int32 | int64]( constraints numberRule[T], bits int, schema map[string]interface{}, ) { numberSchema := map[string]interface{}{ "type": jsInteger, } minVal := -(1 << (bits - 1)) maxExclVal := uint64(1) << (bits - 1) var orNumberSchema map[string]interface{} generateConstInValidation(constraints, numberSchema) switch { case constraints.HasGt(): var isOr bool switch { case constraints.HasLt(): isOr = constraints.GetLt() <= constraints.GetGt() case constraints.HasLte(): isOr = constraints.GetLte() <= constraints.GetGt() } if isOr { orNumberSchema = map[string]interface{}{"exclusiveMinimum": constraints.GetGt()} } else { numberSchema["exclusiveMinimum"] = constraints.GetGt() } case constraints.HasGte(): var isOr bool switch { case constraints.HasLt(): isOr = constraints.GetLt() <= constraints.GetGte() case constraints.HasLte(): isOr = constraints.GetLte() < constraints.GetGte() } if isOr { orNumberSchema = map[string]interface{}{"minimum": constraints.GetGte()} } else { numberSchema["minimum"] = constraints.GetGte() } default: numberSchema["minimum"] = minVal } switch { case constraints.HasLt(): numberSchema["exclusiveMaximum"] = constraints.GetLt() case constraints.HasLte(): numberSchema["maximum"] = constraints.GetLte() default: numberSchema["exclusiveMaximum"] = maxExclVal } anyOf := []map[string]interface{}{ numberSchema, } if orNumberSchema != nil { numberSchema["minimum"] = minVal orNumberSchema["exclusiveMaximum"] = maxExclVal orNumberSchema["type"] = jsInteger anyOf = append(anyOf, orNumberSchema) } if bits > 52 { anyOf = append(anyOf, map[string]interface{}{ "type": jsString, "pattern": "^-?[0-9]+$", }) } if len(anyOf) > 1 { schema["anyOf"] = anyOf } else { for key, value := range numberSchema { schema[key] = value } } } func (p *jsonSchemaGenerator) generateInt32Validation(_ protoreflect.FieldDescriptor, constraints *validate.FieldConstraints, schema map[string]interface{}) { switch { default: schema["type"] = jsInteger schema["minimum"] = math.MinInt32 schema["maximum"] = math.MaxInt32 case constraints.GetInt32() != nil: generateIntValidation(constraints.GetInt32(), 32, schema) case constraints.GetSint32() != nil: generateIntValidation(constraints.GetSint32(), 32, schema) case constraints.GetSfixed32() != nil: generateIntValidation(constraints.GetSfixed32(), 32, schema) } } func (p *jsonSchemaGenerator) generateInt64Validation(_ protoreflect.FieldDescriptor, constraints *validate.FieldConstraints, schema map[string]interface{}) { switch { default: schema["anyOf"] = []map[string]interface{}{ {"type": jsInteger, "minimum": math.MinInt64, "exclusiveMaximum": math.Pow(2, 63)}, {"type": jsString, "pattern": "^-?[0-9]+$"}, } case constraints.GetInt64() != nil: generateIntValidation(constraints.GetInt64(), 64, schema) case constraints.GetSint64() != nil: generateIntValidation(constraints.GetSint64(), 64, schema) case constraints.GetSfixed64() != nil: generateIntValidation(constraints.GetSfixed64(), 64, schema) } } func generateUintValidation[T uint32 | uint64]( constraints numberRule[T], bits int, schema map[string]interface{}, ) { numberSchema := map[string]interface{}{ "type": jsInteger, } var orNumberSchema map[string]interface{} maxExclVal := float64(uint64(1)<<(bits-1)) * 2 generateConstInValidation(constraints, numberSchema) switch { case constraints.HasGt(): var isOr bool switch { case constraints.HasLt(): isOr = constraints.GetLt() <= constraints.GetGt() case constraints.HasLte(): isOr = constraints.GetLte() <= constraints.GetGt() } if isOr { orNumberSchema = map[string]interface{}{"exclusiveMinimum": constraints.GetGt()} } else { numberSchema["exclusiveMinimum"] = constraints.GetGt() } case constraints.HasGte(): var isOr bool switch { case constraints.HasLt(): isOr = constraints.GetLt() <= constraints.GetGte() case constraints.HasLte(): isOr = constraints.GetLte() < constraints.GetGte() } if isOr { orNumberSchema = map[string]interface{}{"minimum": constraints.GetGte()} } else { numberSchema["minimum"] = constraints.GetGte() } default: numberSchema["minimum"] = 0 } switch { case constraints.HasLt(): numberSchema["exclusiveMaximum"] = constraints.GetLt() case constraints.HasLte(): numberSchema["maximum"] = constraints.GetLte() default: numberSchema["exclusiveMaximum"] = maxExclVal } anyOf := []map[string]interface{}{ numberSchema, } if bits > 52 { anyOf = append(anyOf, map[string]interface{}{ "type": jsString, "pattern": "^[0-9]+$", }) } if orNumberSchema != nil { numberSchema["minimum"] = 0 orNumberSchema["exclusiveMaximum"] = maxExclVal orNumberSchema["type"] = jsInteger anyOf = append(anyOf, orNumberSchema) } if len(anyOf) > 1 { schema["anyOf"] = anyOf } else { for key, value := range numberSchema { schema[key] = value } } } func (p *jsonSchemaGenerator) generateUint32Validation(_ protoreflect.FieldDescriptor, constraints *validate.FieldConstraints, schema map[string]interface{}) { switch { default: schema["type"] = jsInteger schema["minimum"] = 0 schema["maximum"] = math.MaxUint32 case constraints.GetUint32() != nil: generateUintValidation(constraints.GetUint32(), 32, schema) case constraints.GetFixed32() != nil: generateUintValidation(constraints.GetFixed32(), 32, schema) } } func (p *jsonSchemaGenerator) generateUint64Validation(_ protoreflect.FieldDescriptor, constraints *validate.FieldConstraints, schema map[string]interface{}) { switch { default: schema["anyOf"] = []map[string]interface{}{ {"type": jsInteger, "minimum": 0, "exclusiveMaximum": float64(uint64(1)<<63) * 2}, {"type": jsString, "pattern": "^[0-9]+$"}, } case constraints.GetUint64() != nil: generateUintValidation(constraints.GetUint64(), 64, schema) case constraints.GetFixed64() != nil: generateUintValidation(constraints.GetFixed64(), 64, schema) } } // nolint: gocyclo func (p *jsonSchemaGenerator) generateFloatValidation(_ protoreflect.FieldDescriptor, constraints *validate.FieldConstraints, schema map[string]interface{}, bits int) { includePosInf := true includeNegInf := true includeNaN := true numberSchema := map[string]interface{}{ "type": jsNumber, } var orNumberSchema map[string]interface{} switch { default: if bits == 32 { numberSchema["minimum"] = -math.MaxFloat32 numberSchema["maximum"] = math.MaxFloat32 } case constraints.GetFloat() != nil: if constraints.GetFloat().GetFinite() { includePosInf = false includeNegInf = false includeNaN = false } if constraints.GetFloat().Const != nil { numberSchema["enum"] = []float32{constraints.GetFloat().GetConst()} includePosInf = false includeNegInf = false includeNaN = false if math.IsInf(float64(constraints.GetFloat().GetConst()), 1) { includePosInf = true } if math.IsInf(float64(constraints.GetFloat().GetConst()), -1) { includeNegInf = true } if math.IsNaN(float64(constraints.GetFloat().GetConst())) { includeNaN = true } } if len(constraints.GetFloat().GetIn()) > 0 { numberSchema["enum"] = constraints.GetFloat().GetIn() includePosInf = false includeNegInf = false includeNaN = false for _, value := range constraints.GetFloat().GetIn() { if math.IsInf(float64(value), 1) { includePosInf = true } if math.IsInf(float64(value), -1) { includeNegInf = true } if math.IsNaN(float64(value)) { includeNaN = true } } } switch greaterThan := constraints.GetFloat().GetGreaterThan().(type) { case *validate.FloatRules_Gt: includeNaN = false var isOr bool switch lessThan := constraints.GetFloat().GetLessThan().(type) { case *validate.FloatRules_Lt: isOr = lessThan.Lt <= greaterThan.Gt case *validate.FloatRules_Lte: isOr = lessThan.Lte <= greaterThan.Gt } if isOr { orNumberSchema = map[string]interface{}{ "type": jsNumber, "exclusiveMinimum": greaterThan.Gt, } } else { includeNegInf = false numberSchema["exclusiveMinimum"] = greaterThan.Gt } case *validate.FloatRules_Gte: includeNaN = false isOr := false switch lessThan := constraints.GetFloat().GetLessThan().(type) { case *validate.FloatRules_Lt: isOr = lessThan.Lt <= greaterThan.Gte case *validate.FloatRules_Lte: isOr = lessThan.Lte < greaterThan.Gte } if isOr { orNumberSchema = map[string]interface{}{ "type": jsNumber, "minimum": greaterThan.Gte, } } else { if greaterThan.Gte != float32(math.Inf(-1)) { includeNegInf = false } numberSchema["minimum"] = greaterThan.Gte } default: numberSchema["minimum"] = -math.MaxFloat32 } switch lessThan := constraints.GetFloat().GetLessThan().(type) { case *validate.FloatRules_Lt: includeNaN = false if orNumberSchema == nil { includePosInf = false } numberSchema["exclusiveMaximum"] = lessThan.Lt case *validate.FloatRules_Lte: includeNaN = false if lessThan.Lte != float32(math.Inf(1)) && orNumberSchema == nil { includePosInf = false } numberSchema["maximum"] = lessThan.Lte default: numberSchema["maximum"] = math.MaxFloat32 } case constraints.GetDouble() != nil: if constraints.GetDouble().GetFinite() { includePosInf = false includeNegInf = false includeNaN = false } if constraints.GetDouble().Const != nil { numberSchema["enum"] = []float64{constraints.GetDouble().GetConst()} includePosInf = false includeNegInf = false includeNaN = false if math.IsInf(constraints.GetDouble().GetConst(), 1) { includePosInf = true } if math.IsInf(constraints.GetDouble().GetConst(), -1) { includeNegInf = true } if math.IsNaN(constraints.GetDouble().GetConst()) { includeNaN = true } } if len(constraints.GetDouble().GetIn()) > 0 { numberSchema["enum"] = constraints.GetDouble().GetIn() includePosInf = false includeNegInf = false includeNaN = false for _, value := range constraints.GetDouble().GetIn() { if math.IsInf(value, 1) { includePosInf = true } if math.IsInf(value, -1) { includeNegInf = true } if math.IsNaN(value) { includeNaN = true } } } switch greaterThan := constraints.GetDouble().GetGreaterThan().(type) { case *validate.DoubleRules_Gt: includeNaN = false var isOr bool switch lessThan := constraints.GetDouble().GetLessThan().(type) { case *validate.DoubleRules_Lt: isOr = lessThan.Lt <= greaterThan.Gt case *validate.DoubleRules_Lte: isOr = lessThan.Lte <= greaterThan.Gt } if isOr { orNumberSchema = map[string]interface{}{ "type": jsNumber, "exclusiveMinimum": greaterThan.Gt, } } else { includeNegInf = false numberSchema["exclusiveMinimum"] = greaterThan.Gt } case *validate.DoubleRules_Gte: includeNaN = false isOr := false switch lessThan := constraints.GetDouble().GetLessThan().(type) { case *validate.DoubleRules_Lt: isOr = lessThan.Lt <= greaterThan.Gte case *validate.DoubleRules_Lte: isOr = lessThan.Lte < greaterThan.Gte } if isOr { orNumberSchema = map[string]interface{}{ "type": jsNumber, "minimum": greaterThan.Gte, } } else { if greaterThan.Gte != math.Inf(-1) { includeNegInf = false } numberSchema["minimum"] = greaterThan.Gte } } switch lessThan := constraints.GetDouble().GetLessThan().(type) { case *validate.DoubleRules_Lt: includeNaN = false if orNumberSchema == nil { includePosInf = false } numberSchema["exclusiveMaximum"] = lessThan.Lt case *validate.DoubleRules_Lte: includeNaN = false if lessThan.Lte != math.Inf(1) && orNumberSchema == nil { includePosInf = false } numberSchema["maximum"] = lessThan.Lte } } anyOf := []map[string]interface{}{ numberSchema, } if orNumberSchema != nil { anyOf = append(anyOf, orNumberSchema) } extremaEnum := []interface{}{} if includePosInf { extremaEnum = append(extremaEnum, "Infinity") } if includeNegInf { extremaEnum = append(extremaEnum, "-Infinity") } if includeNaN { extremaEnum = append(extremaEnum, "NaN") } if len(extremaEnum) > 0 { anyOf = append(anyOf, map[string]interface{}{ "type": jsString, "enum": extremaEnum, }, map[string]interface{}{ "type": jsString, // Allow other form of NaN, -Infinity, and Infinity. }) } else { anyOf = append(anyOf, map[string]interface{}{ "type": jsString, "pattern": "^-?[0-9]+(\\.[0-9]+)?([eE][+-]?[0-9]+)?$", }) } schema["anyOf"] = anyOf } const ( ipv4PatternBit = "((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" ipv6PatternBit = "(([0-9a-fA-F]{1,4}::?){1,7}([0-9a-fA-F]{1,4})|([0-9a-fA-F]{1,4}:){1,7}:|:((([0-9a-fA-F]{1,4}:){1,6})?[0-9a-fA-F]{1,4})?|::)" ipv4LenPatternBit = "/([0-9]|[12][0-9]|3[0-2])" ipv6LenPatternBit = "/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])" portPatternBit = "([1-9][0-9]{0,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])" hostnamePatternBit = "[A-Za-z0-9][A-Za-z0-9-]{0,63}(\\.[A-Za-z0-9-][A-Za-z0-9-]{0,63})*" ipv4Pattern = "^" + ipv4PatternBit + "$" ipv6Pattern = "^" + ipv6PatternBit + "$" hostnamePattern = "^" + hostnamePatternBit + "$" uriPattern = "^(?:(?:[a-zA-Z][a-zA-Z\\d+\\-.]*):)?(?://(?:[A-Za-z0-9\\-\\.]+(?::\\d+)?))?(/[^\\?#]*)?(?:\\?([^\\#]*))?(?:\\#(.*))?$" uriRefPattern = "^(?:(?:[a-zA-Z][a-zA-Z\\d+\\-.]*):)?(?:\\/\\/(?:[A-Za-z0-9\\-\\.]+(?::\\d+)?))?(/[^\\?#]*)?(?:\\?([^\\#]*))?(?:\\#(.*))?$" uuidPattern = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" tuuidPattern = "^[0-9a-fA-F]{32}$" ipv4PrefixLenPattern = "^" + ipv4PatternBit + ipv4LenPatternBit + "$" ipv6PrefixLenPattern = "^" + ipv6PatternBit + ipv6LenPatternBit + "$" ipv4PrefixPattern = "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}0/([0-9]|[12][0-9]|3[0-2])$" ipv6PrefixPattern = "^(([0-9a-fA-F]{1,4}:){1,7}:|::)/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$" hostAndPortPattern = "^(" + hostnamePatternBit + "|" + ipv4PatternBit + "|\\[" + ipv6PatternBit + "\\]):" + portPatternBit + "$" ) // nolint: gocyclo func generateWellKnownPattern(constraints *validate.FieldConstraints, schema map[string]interface{}) { switch wellKnown := constraints.GetString().GetWellKnown().(type) { case *validate.StringRules_Hostname: if wellKnown.Hostname { schema["pattern"] = hostnamePattern } case *validate.StringRules_Email: if wellKnown.Email { schema["format"] = "email" } case *validate.StringRules_Ip: if wellKnown.Ip { schema["pattern"] = fmt.Sprintf("%s|%s", ipv4Pattern, ipv6Pattern) } case *validate.StringRules_Ipv4: if wellKnown.Ipv4 { schema["format"] = "ipv4" } case *validate.StringRules_Ipv6: if wellKnown.Ipv6 { schema["format"] = "ipv6" } case *validate.StringRules_Uri: if wellKnown.Uri { schema["pattern"] = uriPattern } case *validate.StringRules_UriRef: if wellKnown.UriRef { schema["pattern"] = uriRefPattern } case *validate.StringRules_Address: if wellKnown.Address { schema["pattern"] = fmt.Sprintf("%s|%s|%s", ipv4Pattern, ipv6Pattern, hostnamePattern) } case *validate.StringRules_Uuid: if wellKnown.Uuid { schema["pattern"] = uuidPattern } case *validate.StringRules_Tuuid: if wellKnown.Tuuid { schema["pattern"] = tuuidPattern } case *validate.StringRules_Ipv4WithPrefixlen: if wellKnown.Ipv4WithPrefixlen { schema["pattern"] = ipv4PrefixLenPattern } case *validate.StringRules_Ipv6WithPrefixlen: if wellKnown.Ipv6WithPrefixlen { schema["pattern"] = ipv6PrefixLenPattern } case *validate.StringRules_IpWithPrefixlen: if wellKnown.IpWithPrefixlen { schema["pattern"] = fmt.Sprintf("%s|%s", ipv4PrefixLenPattern, ipv6PrefixLenPattern) } case *validate.StringRules_Ipv4Prefix: if wellKnown.Ipv4Prefix { schema["pattern"] = ipv4PrefixPattern } case *validate.StringRules_Ipv6Prefix: if wellKnown.Ipv6Prefix { schema["pattern"] = ipv6PrefixPattern } case *validate.StringRules_IpPrefix: if wellKnown.IpPrefix { schema["pattern"] = fmt.Sprintf("%s|%s", ipv4PrefixPattern, ipv6PrefixPattern) } case *validate.StringRules_HostAndPort: if wellKnown.HostAndPort { schema["pattern"] = hostAndPortPattern } case *validate.StringRules_WellKnownRegex: if wellKnown.WellKnownRegex == validate.KnownRegex_KNOWN_REGEX_HTTP_HEADER_NAME && constraints.GetString().GetStrict() { schema["pattern"] = "^:?[0-9a-zA-Z!#$%&\\'*+-.^_|~\\x60]+$" } } } func (p *jsonSchemaGenerator) generateStringValidation(_ protoreflect.FieldDescriptor, constraints *validate.FieldConstraints, schema map[string]interface{}) { schema["type"] = jsString if constraints.GetString() == nil { return } // Bytes are <= Characters, so we can only enforce an upper bound. if constraints.GetString().LenBytes != nil { schema["maxLength"] = constraints.GetString().GetMaxBytes() } else if constraints.GetString().MaxBytes != nil { schema["maxLength"] = constraints.GetString().GetMaxBytes() } if constraints.GetString().Len != nil { schema["minLength"] = constraints.GetString().GetLen() schema["maxLength"] = constraints.GetString().GetLen() } else { if constraints.GetString().MinLen != nil && constraints.GetString().GetMinLen() > 0 { schema["minLength"] = constraints.GetString().GetMinLen() } else if constraints.GetRequired() && constraints.GetIgnore() != validate.Ignore_IGNORE_IF_DEFAULT_VALUE { schema["minLength"] = 1 } if constraints.GetString().MaxLen != nil { schema["maxLength"] = constraints.GetString().GetMaxLen() } } generateWellKnownPattern(constraints, schema) switch { case constraints.GetString().Pattern != nil: schema["pattern"] = constraints.GetString().GetPattern() case constraints.GetString().Prefix != nil, constraints.GetString().Suffix != nil, constraints.GetString().Contains != nil: pattern := "" if constraints.GetString().Prefix != nil { pattern += "^" + constraints.GetString().GetPrefix() } pattern += ".*" if constraints.GetString().Contains != nil { pattern += constraints.GetString().GetContains() pattern += ".*" } if constraints.GetString().Suffix != nil { pattern += constraints.GetString().GetSuffix() + "$" } schema["pattern"] = pattern } if constraints.GetString().Const != nil { schema["enum"] = []string{constraints.GetString().GetConst()} } else if len(constraints.GetString().In) > 0 { schema["enum"] = constraints.GetString().GetIn() } } func base64EncodedLength(inputSize uint64) (uint64, uint64) { // Base64 encoding is 4/3 the size of the input. // Padding is added to make the output size a multiple of 4. // For example 5 bytes is encoded as characters := inputSize * 4 / 3 if inputSize%3 != 0 { characters++ } padding := 4 - (characters % 4) return characters, padding } func (p *jsonSchemaGenerator) generateBytesValidation(_ protoreflect.FieldDescriptor, constraints *validate.FieldConstraints, schema map[string]interface{}) { schema["type"] = jsString // Set a regex to match base64 encoded strings. schema["pattern"] = "^[A-Za-z0-9+/]*={0,2}$" if constraints.GetBytes() == nil { return } if constraints.GetBytes().Len != nil { size, padding := base64EncodedLength(constraints.GetBytes().GetLen()) schema["minLength"] = size schema["maxLength"] = size + padding } else { if constraints.GetBytes().MaxLen != nil { size, padding := base64EncodedLength(constraints.GetBytes().GetMaxLen()) schema["maxLength"] = size + padding } if constraints.GetBytes().MinLen != nil { size, _ := base64EncodedLength(constraints.GetBytes().GetMinLen()) schema["minLength"] = size } else if constraints.GetRequired() && constraints.GetIgnore() != validate.Ignore_IGNORE_IF_DEFAULT_VALUE { schema["minLength"] = 1 } } } func (p *jsonSchemaGenerator) generateMessageValidation(field protoreflect.FieldDescriptor, schema map[string]interface{}) { p.generate(field.Message(), schema) } func (p *jsonSchemaGenerator) generateWrapperValidation( desc protoreflect.MessageDescriptor, constraints *validate.FieldConstraints, schema map[string]interface{}, ) { field := desc.Fields().Get(0) p.setDescription(field, schema) p.generateValidation(field, constraints, schema) } func (p *jsonSchemaGenerator) makeWktGenerators() map[protoreflect.FullName]func(protoreflect.MessageDescriptor, *validate.FieldConstraints, map[string]interface{}) { var result = make(map[protoreflect.FullName]func(protoreflect.MessageDescriptor, *validate.FieldConstraints, map[string]interface{})) result["google.protobuf.Any"] = func(_ protoreflect.MessageDescriptor, _ *validate.FieldConstraints, schema map[string]interface{}) { schema["type"] = jsObject schema["properties"] = map[string]interface{}{ "@type": map[string]interface{}{ "type": "string", }, } } result["google.protobuf.Duration"] = func(_ protoreflect.MessageDescriptor, _ *validate.FieldConstraints, schema map[string]interface{}) { schema["type"] = jsString schema["format"] = "duration" } result["google.protobuf.Timestamp"] = func(_ protoreflect.MessageDescriptor, _ *validate.FieldConstraints, schema map[string]interface{}) { schema["type"] = jsString schema["format"] = "date-time" } result["google.protobuf.Value"] = func(_ protoreflect.MessageDescriptor, _ *validate.FieldConstraints, _ map[string]interface{}) {} result["google.protobuf.ListValue"] = func(_ protoreflect.MessageDescriptor, _ *validate.FieldConstraints, schema map[string]interface{}) { schema["type"] = jsArray } result["google.protobuf.NullValue"] = func(_ protoreflect.MessageDescriptor, _ *validate.FieldConstraints, schema map[string]interface{}) { schema["type"] = jsNull } result["google.protobuf.Struct"] = func(_ protoreflect.MessageDescriptor, _ *validate.FieldConstraints, schema map[string]interface{}) { schema["type"] = jsObject } result["google.protobuf.BoolValue"] = p.generateWrapperValidation result["google.protobuf.BytesValue"] = p.generateWrapperValidation result["google.protobuf.DoubleValue"] = p.generateWrapperValidation result["google.protobuf.FloatValue"] = p.generateWrapperValidation result["google.protobuf.Int32Value"] = p.generateWrapperValidation result["google.protobuf.Int64Value"] = p.generateWrapperValidation result["google.protobuf.StringValue"] = p.generateWrapperValidation result["google.protobuf.UInt32Value"] = p.generateWrapperValidation result["google.protobuf.UInt64Value"] = p.generateWrapperValidation return result } func (p *jsonSchemaGenerator) shouldIgnoreField(fdesc protoreflect.FieldDescriptor) FieldVisibility { const ignoreComment = "jsonschema:ignore" const hideComment = "jsonschema:hide" srcLoc := fdesc.ParentFile().SourceLocations().ByDescriptor(fdesc) switch { case strings.Contains(srcLoc.LeadingComments, ignoreComment), strings.Contains(srcLoc.TrailingComments, ignoreComment): return FieldIgnore case strings.Contains(srcLoc.LeadingComments, hideComment), strings.Contains(srcLoc.TrailingComments, hideComment): return FieldHide default: return FieldVisible } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/adiom-data/grpcmcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server