github.com/kaptinlin/jsonschema@v0.4.6/struct_validation.go (about) 1 package jsonschema 2 3 import ( 4 "reflect" 5 "regexp" 6 "strings" 7 "sync" 8 "time" 9 ) 10 11 // FieldCache stores parsed field information for a struct type 12 type FieldCache struct { 13 FieldsByName map[string]FieldInfo 14 FieldCount int 15 } 16 17 // FieldInfo contains metadata for a struct field 18 type FieldInfo struct { 19 Index int // Field index in the struct 20 JSONName string // JSON field name (after processing tags) 21 Omitempty bool // Whether the field has omitempty tag 22 Type reflect.Type // Field type 23 } 24 25 // Global cache for struct field information 26 var fieldCacheMap sync.Map 27 28 // getFieldCache retrieves or creates cached field information for a struct type 29 func getFieldCache(structType reflect.Type) *FieldCache { 30 if cached, ok := fieldCacheMap.Load(structType); ok { 31 return cached.(*FieldCache) 32 } 33 34 cache := parseStructType(structType) 35 fieldCacheMap.Store(structType, cache) 36 return cache 37 } 38 39 // parseStructType analyzes a struct type and extracts field information 40 func parseStructType(structType reflect.Type) *FieldCache { 41 cache := &FieldCache{ 42 FieldsByName: make(map[string]FieldInfo), 43 } 44 45 for i := 0; i < structType.NumField(); i++ { 46 field := structType.Field(i) 47 48 // Skip unexported fields 49 if !field.IsExported() { 50 continue 51 } 52 53 jsonName, omitempty := parseJSONTag(field.Tag.Get("json"), field.Name) 54 if jsonName == "-" { 55 continue // Skip fields marked with json:"-" 56 } 57 58 cache.FieldsByName[jsonName] = FieldInfo{ 59 Index: i, 60 JSONName: jsonName, 61 Omitempty: omitempty, 62 Type: field.Type, 63 } 64 cache.FieldCount++ 65 } 66 67 return cache 68 } 69 70 // parseJSONTag parses a JSON struct tag and returns the field name and omitempty flag 71 func parseJSONTag(tag, defaultName string) (string, bool) { 72 if tag == "" { 73 return defaultName, false 74 } 75 76 if commaIdx := strings.IndexByte(tag, ','); commaIdx >= 0 { 77 name := tag[:commaIdx] 78 if name == "" { 79 name = defaultName 80 } 81 return name, strings.Contains(tag[commaIdx:], "omitempty") 82 } 83 84 return tag, false 85 } 86 87 // isEmptyValue checks if a reflect.Value represents an empty value for omitempty behavior 88 func isEmptyValue(rv reflect.Value) bool { 89 switch rv.Kind() { 90 case reflect.Invalid: 91 return true 92 case reflect.Array, reflect.Map, reflect.Slice, reflect.String: 93 return rv.Len() == 0 94 case reflect.Bool: 95 return !rv.Bool() // For omitempty, false is considered empty 96 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 97 return rv.Int() == 0 98 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 99 return rv.Uint() == 0 100 case reflect.Float32, reflect.Float64: 101 return rv.Float() == 0 102 case reflect.Interface, reflect.Ptr: 103 return rv.IsNil() 104 case reflect.Struct: 105 // Special handling for time.Time 106 if rv.Type() == reflect.TypeOf(time.Time{}) { 107 t := rv.Interface().(time.Time) 108 return t.IsZero() 109 } 110 return rv.IsZero() 111 case reflect.Uintptr, reflect.Complex64, reflect.Complex128, reflect.Chan, reflect.Func, reflect.UnsafePointer: 112 return false 113 default: 114 return false 115 } 116 } 117 118 // isMissingValue checks if a reflect.Value represents a missing value for required validation 119 func isMissingValue(rv reflect.Value) bool { 120 switch rv.Kind() { 121 case reflect.Invalid: 122 return true 123 case reflect.Interface, reflect.Ptr: 124 return rv.IsNil() 125 case reflect.Struct: 126 // Special handling for time.Time 127 if rv.Type() == reflect.TypeOf(time.Time{}) { 128 t := rv.Interface().(time.Time) 129 return t.IsZero() 130 } 131 return rv.IsZero() 132 case reflect.String: 133 // For required fields, empty string is considered missing 134 return rv.String() == "" 135 case reflect.Slice, reflect.Map, reflect.Array: 136 // For required fields, empty collections are considered missing 137 return rv.Len() == 0 138 case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, 139 reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, 140 reflect.Float32, reflect.Float64, reflect.Uintptr, reflect.Complex64, reflect.Complex128, 141 reflect.Chan, reflect.Func, reflect.UnsafePointer: 142 // For required fields, any non-nil value is considered present 143 // This includes false for booleans, 0 for numbers, etc. 144 return false 145 default: 146 // For required fields, any non-nil value is considered present 147 // This includes false for booleans, 0 for numbers, etc. 148 return false 149 } 150 } 151 152 // extractValue safely gets the interface{} value from a reflect.Value 153 func extractValue(rv reflect.Value) interface{} { 154 // Handle pointers by dereferencing them first 155 for rv.Kind() == reflect.Ptr { 156 if rv.IsNil() { 157 return nil 158 } 159 rv = rv.Elem() 160 } 161 162 // Special handling for time.Time - convert to string for JSON schema validation 163 if rv.Type() == reflect.TypeOf(time.Time{}) { 164 t := rv.Interface().(time.Time) 165 return t.Format(time.RFC3339) 166 } 167 168 if rv.CanInterface() { 169 return rv.Interface() 170 } 171 172 return nil 173 } 174 175 // evaluateObjectStruct handles validation for Go structs 176 func evaluateObjectStruct(schema *Schema, structValue reflect.Value, evaluatedProps map[string]bool, evaluatedItems map[int]bool, dynamicScope *DynamicScope) ([]*EvaluationResult, []*EvaluationError) { 177 results := []*EvaluationResult{} 178 errors := []*EvaluationError{} 179 180 structType := structValue.Type() 181 fieldCache := getFieldCache(structType) 182 183 // Validate properties 184 if schema.Properties != nil { 185 propertiesResults, propertiesErrors := evaluatePropertiesStruct(schema, structValue, fieldCache, evaluatedProps, dynamicScope) 186 results = append(results, propertiesResults...) 187 errors = append(errors, propertiesErrors...) 188 } 189 190 // Validate patternProperties 191 if schema.PatternProperties != nil { 192 patternResults, patternError := evaluatePatternPropertiesStruct(schema, structValue, fieldCache, evaluatedProps, dynamicScope) 193 if patternResults != nil { 194 results = append(results, patternResults...) 195 } 196 if patternError != nil { 197 errors = append(errors, patternError) 198 } 199 } 200 201 // Validate additionalProperties 202 if schema.AdditionalProperties != nil { 203 additionalResults, additionalError := evaluateAdditionalPropertiesStruct(schema, structValue, fieldCache, evaluatedProps, dynamicScope) 204 if additionalResults != nil { 205 results = append(results, additionalResults...) 206 } 207 if additionalError != nil { 208 errors = append(errors, additionalError) 209 } 210 } 211 212 // Validate propertyNames 213 if schema.PropertyNames != nil { 214 propertyNamesResults, propertyNamesError := evaluatePropertyNamesStruct(schema, structValue, fieldCache, evaluatedProps, dynamicScope) 215 if propertyNamesResults != nil { 216 results = append(results, propertyNamesResults...) 217 } 218 if propertyNamesError != nil { 219 errors = append(errors, propertyNamesError) 220 } 221 } 222 223 // Validate required fields 224 if len(schema.Required) > 0 { 225 if err := evaluateRequiredStruct(schema, structValue, fieldCache); err != nil { 226 errors = append(errors, err) 227 } 228 } 229 230 // Validate dependentRequired 231 if len(schema.DependentRequired) > 0 { 232 if err := evaluateDependentRequiredStruct(schema, structValue, fieldCache); err != nil { 233 errors = append(errors, err) 234 } 235 } 236 237 // Validate property count constraints 238 if schema.MaxProperties != nil || schema.MinProperties != nil { 239 if err := evaluatePropertyCountStruct(schema, structValue, fieldCache); err != nil { 240 errors = append(errors, err) 241 } 242 } 243 244 return results, errors 245 } 246 247 // evaluateObjectReflectMap handles validation for reflect map types 248 func evaluateObjectReflectMap(schema *Schema, mapValue reflect.Value, evaluatedProps map[string]bool, evaluatedItems map[int]bool, dynamicScope *DynamicScope) ([]*EvaluationResult, []*EvaluationError) { 249 // Convert reflect map to map[string]interface{} and use existing logic 250 object := make(map[string]interface{}) 251 252 for _, key := range mapValue.MapKeys() { 253 if key.Kind() == reflect.String { 254 value := mapValue.MapIndex(key) 255 if value.CanInterface() { 256 object[key.String()] = value.Interface() 257 } 258 } 259 } 260 261 return evaluateObjectMap(schema, object, evaluatedProps, evaluatedItems, dynamicScope) 262 } 263 264 // evaluatePropertiesStruct validates struct properties against schema properties 265 func evaluatePropertiesStruct(schema *Schema, structValue reflect.Value, fieldCache *FieldCache, evaluatedProps map[string]bool, dynamicScope *DynamicScope) ([]*EvaluationResult, []*EvaluationError) { 266 results := []*EvaluationResult{} 267 errors := []*EvaluationError{} 268 invalidProperties := []string{} 269 270 for propName, propSchema := range *schema.Properties { 271 evaluatedProps[propName] = true 272 273 fieldInfo, exists := fieldCache.FieldsByName[propName] 274 if !exists { 275 // Field doesn't exist in struct, validate as nil 276 result, _, _ := propSchema.evaluate(nil, dynamicScope) 277 if result != nil { 278 results = append(results, result) 279 if !result.IsValid() { 280 invalidProperties = append(invalidProperties, propName) 281 } 282 } 283 continue 284 } 285 286 // Get field value 287 fieldValue := structValue.Field(fieldInfo.Index) 288 289 // Handle omitempty: skip validation if field is empty and has omitempty tag 290 if fieldInfo.Omitempty && isEmptyValue(fieldValue) { 291 continue 292 } 293 294 // Get the interface value for validation 295 valueToValidate := extractValue(fieldValue) 296 297 result, _, _ := propSchema.evaluate(valueToValidate, dynamicScope) 298 if result != nil { 299 results = append(results, result) 300 if !result.IsValid() { 301 invalidProperties = append(invalidProperties, propName) 302 } 303 } 304 } 305 306 // Handle errors for invalid properties 307 if len(invalidProperties) > 0 { 308 errors = append(errors, createPropertyValidationError(invalidProperties)) 309 } 310 311 return results, errors 312 } 313 314 // evaluateRequiredStruct validates required fields for structs 315 func evaluateRequiredStruct(schema *Schema, structValue reflect.Value, fieldCache *FieldCache) *EvaluationError { 316 missingFields := []string{} 317 318 for _, requiredField := range schema.Required { 319 fieldInfo, exists := fieldCache.FieldsByName[requiredField] 320 if !exists { 321 missingFields = append(missingFields, requiredField) 322 continue 323 } 324 325 fieldValue := structValue.Field(fieldInfo.Index) 326 327 // Check if field is missing or empty 328 if !fieldValue.IsValid() { 329 missingFields = append(missingFields, requiredField) 330 } else { 331 // For required fields, use the specific missing check 332 isMissing := isMissingValue(fieldValue) 333 334 // If the field is missing, it's required but missing 335 if isMissing { 336 missingFields = append(missingFields, requiredField) 337 } 338 } 339 } 340 341 return createRequiredValidationError(missingFields) 342 } 343 344 // evaluatePropertyCountStruct validates maxProperties and minProperties for structs 345 func evaluatePropertyCountStruct(schema *Schema, structValue reflect.Value, fieldCache *FieldCache) *EvaluationError { 346 // Count actual non-empty properties (considering omitempty) 347 actualCount := 0 348 for _, fieldInfo := range fieldCache.FieldsByName { 349 fieldValue := structValue.Field(fieldInfo.Index) 350 if !fieldInfo.Omitempty || !isEmptyValue(fieldValue) { 351 actualCount++ 352 } 353 } 354 355 if schema.MaxProperties != nil && float64(actualCount) > *schema.MaxProperties { 356 return NewEvaluationError("maxProperties", "too_many_properties", 357 "Value should have at most {max_properties} properties", map[string]interface{}{ 358 "max_properties": *schema.MaxProperties, 359 }) 360 } 361 362 if schema.MinProperties != nil && float64(actualCount) < *schema.MinProperties { 363 return NewEvaluationError("minProperties", "too_few_properties", 364 "Value should have at least {min_properties} properties", map[string]interface{}{ 365 "min_properties": *schema.MinProperties, 366 }) 367 } 368 369 return nil 370 } 371 372 // evaluatePatternPropertiesStruct validates struct properties against pattern properties 373 func evaluatePatternPropertiesStruct(schema *Schema, structValue reflect.Value, fieldCache *FieldCache, evaluatedProps map[string]bool, dynamicScope *DynamicScope) ([]*EvaluationResult, *EvaluationError) { 374 results := []*EvaluationResult{} 375 376 for jsonName, fieldInfo := range fieldCache.FieldsByName { 377 if evaluatedProps[jsonName] { 378 continue 379 } 380 381 fieldValue := structValue.Field(fieldInfo.Index) 382 if fieldInfo.Omitempty && isEmptyValue(fieldValue) { 383 continue 384 } 385 386 for pattern, patternSchema := range *schema.PatternProperties { 387 if matched, _ := regexp.MatchString(pattern, jsonName); matched { 388 evaluatedProps[jsonName] = true 389 value := extractValue(fieldValue) 390 391 // Reuse existing validation logic directly 392 result, _, _ := patternSchema.evaluate(value, dynamicScope) 393 if result != nil { 394 results = append(results, result) 395 } 396 break 397 } 398 } 399 } 400 401 return results, nil 402 } 403 404 // evaluateAdditionalPropertiesStruct validates struct properties against additional properties 405 func evaluateAdditionalPropertiesStruct(schema *Schema, structValue reflect.Value, fieldCache *FieldCache, evaluatedProps map[string]bool, dynamicScope *DynamicScope) ([]*EvaluationResult, *EvaluationError) { 406 results := []*EvaluationResult{} 407 invalidProperties := []string{} 408 409 // Check for unevaluated properties 410 for jsonName, fieldInfo := range fieldCache.FieldsByName { 411 if evaluatedProps[jsonName] { 412 continue 413 } 414 415 fieldValue := structValue.Field(fieldInfo.Index) 416 if fieldInfo.Omitempty && isEmptyValue(fieldValue) { 417 continue 418 } 419 420 // This is an additional property, validate according to additionalProperties 421 if schema.AdditionalProperties != nil { 422 value := extractValue(fieldValue) 423 result, _, _ := schema.AdditionalProperties.evaluate(value, dynamicScope) 424 if result != nil { 425 results = append(results, result) 426 if !result.IsValid() { 427 invalidProperties = append(invalidProperties, jsonName) 428 } 429 } 430 // Mark property as evaluated 431 evaluatedProps[jsonName] = true 432 } 433 } 434 435 // Handle errors for invalid properties 436 if len(invalidProperties) > 0 { 437 return results, createValidationError( 438 "additional_property_mismatch", 439 "additionalProperties", 440 "Additional property {property} does not match the schema", 441 "Additional properties {properties} do not match the schema", 442 invalidProperties, 443 ) 444 } 445 446 return results, nil 447 } 448 449 // evaluatePropertyNamesStruct validates struct property names 450 func evaluatePropertyNamesStruct(schema *Schema, structValue reflect.Value, fieldCache *FieldCache, evaluatedProps map[string]bool, dynamicScope *DynamicScope) ([]*EvaluationResult, *EvaluationError) { 451 if schema.PropertyNames == nil { 452 return nil, nil 453 } 454 455 results := []*EvaluationResult{} 456 invalidProperties := []string{} 457 458 for jsonName, fieldInfo := range fieldCache.FieldsByName { 459 fieldValue := structValue.Field(fieldInfo.Index) 460 if fieldInfo.Omitempty && isEmptyValue(fieldValue) { 461 continue 462 } 463 464 // Validate the property name itself 465 result, _, _ := schema.PropertyNames.evaluate(jsonName, dynamicScope) 466 if result != nil { 467 results = append(results, result) 468 if !result.IsValid() { 469 invalidProperties = append(invalidProperties, jsonName) 470 } 471 } 472 } 473 474 // Handle errors for invalid properties 475 if len(invalidProperties) > 0 { 476 return results, createValidationError( 477 "property_name_mismatch", 478 "propertyNames", 479 "Property name {property} does not match the schema", 480 "Property names {properties} do not match the schema", 481 invalidProperties, 482 ) 483 } 484 485 return results, nil 486 } 487 488 // evaluateDependentRequiredStruct validates dependent required properties for structs 489 func evaluateDependentRequiredStruct(schema *Schema, structValue reflect.Value, fieldCache *FieldCache) *EvaluationError { 490 for propName, dependentRequired := range schema.DependentRequired { 491 // Check if property exists 492 fieldInfo, exists := fieldCache.FieldsByName[propName] 493 if !exists { 494 continue 495 } 496 497 fieldValue := structValue.Field(fieldInfo.Index) 498 499 // If property exists and is not empty, check dependent properties 500 if !isEmptyValue(fieldValue) { 501 for _, requiredProp := range dependentRequired { 502 depFieldInfo, depExists := fieldCache.FieldsByName[requiredProp] 503 if !depExists { 504 return NewEvaluationError("dependentRequired", "dependent_required_missing", 505 "Property {property} is required when {dependent_property} is present", map[string]interface{}{ 506 "property": requiredProp, 507 "dependent_property": propName, 508 }) 509 } 510 511 depFieldValue := structValue.Field(depFieldInfo.Index) 512 if isMissingValue(depFieldValue) { 513 return NewEvaluationError("dependentRequired", "dependent_required_missing", 514 "Property {property} is required when {dependent_property} is present", map[string]interface{}{ 515 "property": requiredProp, 516 "dependent_property": propName, 517 }) 518 } 519 } 520 } 521 } 522 523 return nil 524 } 525 526 // createValidationError creates a validation error with proper formatting for single or multiple items 527 func createValidationError(errorType, keyword string, singleTemplate, multiTemplate string, invalidItems []string) *EvaluationError { 528 if len(invalidItems) == 1 { 529 return NewEvaluationError(keyword, errorType, singleTemplate, map[string]interface{}{ 530 "property": invalidItems[0], 531 }) 532 } else if len(invalidItems) > 1 { 533 return NewEvaluationError(keyword, errorType, multiTemplate, map[string]interface{}{ 534 "properties": strings.Join(invalidItems, ", "), 535 }) 536 } 537 return nil 538 } 539 540 // createPropertyValidationError creates a validation error for property validation 541 func createPropertyValidationError(invalidProperties []string) *EvaluationError { 542 return createValidationError( 543 "property_mismatch", 544 "properties", 545 "Property {property} does not match the schema", 546 "Properties {properties} do not match their schemas", 547 invalidProperties, 548 ) 549 } 550 551 // createRequiredValidationError creates a validation error for required field validation 552 func createRequiredValidationError(missingFields []string) *EvaluationError { 553 return createValidationError( 554 "required_missing", 555 "required", 556 "Required property {property} is missing", 557 "Required properties {properties} are missing", 558 missingFields, 559 ) 560 }