github.com/kaptinlin/jsonschema@v0.4.6/struct_validation_test.go (about) 1 package jsonschema 2 3 import ( 4 "encoding/json" 5 "testing" 6 "time" 7 ) 8 9 // ============================================================================= 10 // Test Struct Definitions 11 // ============================================================================= 12 13 type BasicUser struct { 14 Name string `json:"name"` 15 Age int `json:"age,omitempty"` 16 SDK string `json:"sdk,omitempty"` 17 } 18 19 type ComplexUser struct { 20 ID int64 `json:"id"` 21 Name string `json:"name"` 22 Email string `json:"email"` 23 Age *int `json:"age,omitempty"` 24 IsActive bool `json:"is_active"` 25 Balance float64 `json:"balance"` 26 Tags []string `json:"tags,omitempty"` 27 Metadata map[string]interface{} `json:"metadata,omitempty"` 28 CreatedAt time.Time `json:"created_at"` 29 LastLogin *time.Time `json:"last_login,omitempty"` 30 Preferences UserPreferences `json:"preferences"` 31 } 32 33 type UserPreferences struct { 34 Theme string `json:"theme"` 35 Language string `json:"language"` 36 Timezone string `json:"timezone,omitempty"` 37 Notifications NotificationSettings `json:"notifications"` 38 } 39 40 type NotificationSettings struct { 41 Email bool `json:"email"` 42 SMS bool `json:"sms"` 43 Push bool `json:"push"` 44 } 45 46 type CustomJSONTags struct { 47 PublicName string `json:"public_name"` 48 InternalID int `json:"internal_id,omitempty"` 49 SecretData string `json:"-"` // Should be ignored in validation 50 AliasedField string `json:"alias"` 51 EmptyNameField string `json:",omitempty"` // Use struct field name with omitempty 52 } 53 54 type AllBasicTypes struct { 55 String string `json:"string"` 56 Int int `json:"int"` 57 Int8 int8 `json:"int8"` 58 Int16 int16 `json:"int16"` 59 Int32 int32 `json:"int32"` 60 Int64 int64 `json:"int64"` 61 Uint uint `json:"uint"` 62 Uint8 uint8 `json:"uint8"` 63 Uint16 uint16 `json:"uint16"` 64 Uint32 uint32 `json:"uint32"` 65 Uint64 uint64 `json:"uint64"` 66 Float32 float32 `json:"float32"` 67 Float64 float64 `json:"float64"` 68 Bool bool `json:"bool"` 69 Bytes []byte `json:"bytes"` 70 } 71 72 type PointerTypes struct { 73 StringPtr *string `json:"string_ptr,omitempty"` 74 IntPtr *int `json:"int_ptr,omitempty"` 75 BoolPtr *bool `json:"bool_ptr,omitempty"` 76 Float64Ptr *float64 `json:"float64_ptr,omitempty"` 77 } 78 79 type ArrayTypes struct { 80 StringArray []string `json:"string_array"` 81 IntArray []int `json:"int_array"` 82 UserArray []BasicUser `json:"user_array"` 83 } 84 85 type EmptyStruct struct{} 86 87 type SingleField struct { 88 Value string `json:"value"` 89 } 90 91 // ============================================================================= 92 // Helper Functions 93 // ============================================================================= 94 95 func stringPtr(s string) *string { return &s } 96 func intPtr(i int) *int { return &i } 97 func boolPtr(b bool) *bool { return &b } 98 func float64Ptr(f float64) *float64 { return &f } 99 100 func compileTestSchema(t *testing.T, schemaJSON string) *Schema { 101 t.Helper() 102 compiler := NewCompiler() 103 schema, err := compiler.Compile([]byte(schemaJSON)) 104 if err != nil { 105 t.Fatalf("Failed to compile schema: %v", err) 106 } 107 return schema 108 } 109 110 // ============================================================================= 111 // Core Struct Validation Tests 112 // ============================================================================= 113 114 // TestBasicStructValidation covers fundamental struct validation scenarios 115 func TestBasicStructValidation(t *testing.T) { 116 schemaJSON := `{ 117 "type": "object", 118 "properties": { 119 "name": {"type": "string"}, 120 "age": {"type": "integer"}, 121 "sdk": {"oneOf": [{"type": "string"}, {"type": "null"}]} 122 }, 123 "required": ["name"] 124 }` 125 126 schema := compileTestSchema(t, schemaJSON) 127 128 tests := []struct { 129 name string 130 data BasicUser 131 wantErr bool 132 }{ 133 { 134 name: "valid complete struct", 135 data: BasicUser{Name: "Alice", Age: 30, SDK: "kafka"}, 136 wantErr: false, 137 }, 138 { 139 name: "valid with omitempty fields empty", 140 data: BasicUser{Name: "Bob"}, 141 wantErr: false, 142 }, 143 { 144 name: "missing required field", 145 data: BasicUser{Age: 25, SDK: "kafka"}, 146 wantErr: true, 147 }, 148 } 149 150 for _, tt := range tests { 151 t.Run(tt.name, func(t *testing.T) { 152 result := schema.Validate(tt.data) 153 if (result.IsValid() == false) != tt.wantErr { 154 if tt.wantErr { 155 t.Errorf("Expected validation to fail, but it passed") 156 } else { 157 details, _ := json.MarshalIndent(result.ToList(false), "", " ") 158 t.Errorf("Expected validation to pass, but got errors: %s", string(details)) 159 } 160 } 161 }) 162 } 163 } 164 165 // TestAllBasicTypes ensures all Go basic types are properly handled 166 func TestAllBasicTypes(t *testing.T) { 167 schemaJSON := `{ 168 "type": "object", 169 "properties": { 170 "string": {"type": "string"}, 171 "int": {"type": "integer"}, "int8": {"type": "integer"}, "int16": {"type": "integer"}, 172 "int32": {"type": "integer"}, "int64": {"type": "integer"}, 173 "uint": {"type": "integer"}, "uint8": {"type": "integer"}, "uint16": {"type": "integer"}, 174 "uint32": {"type": "integer"}, "uint64": {"type": "integer"}, 175 "float32": {"type": "number"}, "float64": {"type": "number"}, 176 "bool": {"type": "boolean"}, 177 "bytes": {"type": "array", "items": {"type": "integer"}} 178 }, 179 "required": ["string", "int", "bool"] 180 }` 181 182 schema := compileTestSchema(t, schemaJSON) 183 184 data := AllBasicTypes{ 185 String: "test", Int: 42, Int8: 8, Int16: 16, Int32: 32, Int64: 64, 186 Uint: 100, Uint8: 200, Uint16: 300, Uint32: 400, Uint64: 500, 187 Float32: 3.14, Float64: 2.718, Bool: true, Bytes: []byte{1, 2, 3}, 188 } 189 190 result := schema.Validate(data) 191 if !result.IsValid() { 192 details, _ := json.MarshalIndent(result.ToList(false), "", " ") 193 t.Errorf("Expected validation to pass for all basic types, but got errors: %s", string(details)) 194 } 195 } 196 197 // ============================================================================= 198 // Advanced Type Support Tests 199 // ============================================================================= 200 201 // TestPointerTypes validates pointer handling including nil pointers 202 func TestPointerTypes(t *testing.T) { 203 schemaJSON := `{ 204 "type": "object", 205 "properties": { 206 "string_ptr": {"type": "string"}, "int_ptr": {"type": "integer"}, 207 "bool_ptr": {"type": "boolean"}, "float64_ptr": {"type": "number"} 208 } 209 }` 210 211 schema := compileTestSchema(t, schemaJSON) 212 213 t.Run("with values", func(t *testing.T) { 214 data := PointerTypes{ 215 StringPtr: stringPtr("hello"), IntPtr: intPtr(42), 216 BoolPtr: boolPtr(true), Float64Ptr: float64Ptr(3.14), 217 } 218 result := schema.Validate(data) 219 if !result.IsValid() { 220 details, _ := json.MarshalIndent(result.ToList(false), "", " ") 221 t.Errorf("Expected validation to pass for pointer types with values: %s", string(details)) 222 } 223 }) 224 225 t.Run("with nil pointers", func(t *testing.T) { 226 data := PointerTypes{} // All fields are nil pointers 227 result := schema.Validate(data) 228 if !result.IsValid() { 229 details, _ := json.MarshalIndent(result.ToList(false), "", " ") 230 t.Errorf("Expected validation to pass for nil pointers: %s", string(details)) 231 } 232 }) 233 } 234 235 // TestTimeHandling verifies time.Time is properly converted to RFC3339 strings 236 func TestTimeHandling(t *testing.T) { 237 schemaJSON := `{ 238 "type": "object", 239 "properties": { 240 "created_at": {"type": "string", "format": "date-time"}, 241 "last_login": {"type": "string", "format": "date-time"} 242 }, 243 "required": ["created_at"] 244 }` 245 246 schema := compileTestSchema(t, schemaJSON) 247 248 now := time.Now() 249 lastLogin := now.Add(-24 * time.Hour) 250 251 data := struct { 252 CreatedAt time.Time `json:"created_at"` 253 LastLogin *time.Time `json:"last_login,omitempty"` 254 }{ 255 CreatedAt: now, 256 LastLogin: &lastLogin, 257 } 258 259 result := schema.Validate(data) 260 if !result.IsValid() { 261 details, _ := json.MarshalIndent(result.ToList(false), "", " ") 262 t.Errorf("Expected validation to pass for time types: %s", string(details)) 263 } 264 } 265 266 // ============================================================================= 267 // JSON Tag Support Tests 268 // ============================================================================= 269 270 // TestCustomJSONTags validates comprehensive JSON tag support 271 func TestCustomJSONTags(t *testing.T) { 272 schemaJSON := `{ 273 "type": "object", 274 "properties": { 275 "public_name": {"type": "string"}, 276 "internal_id": {"type": "integer"}, 277 "alias": {"type": "string"}, 278 "EmptyNameField": {"type": "string"} 279 }, 280 "required": ["public_name"] 281 }` 282 283 schema := compileTestSchema(t, schemaJSON) 284 285 data := CustomJSONTags{ 286 PublicName: "test", 287 InternalID: 123, 288 SecretData: "should be ignored due to json:\"-\"", 289 AliasedField: "aliased", 290 EmptyNameField: "uses struct field name", 291 } 292 293 result := schema.Validate(data) 294 if !result.IsValid() { 295 details, _ := json.MarshalIndent(result.ToList(false), "", " ") 296 t.Errorf("Expected validation to pass for custom JSON tags: %s", string(details)) 297 } 298 } 299 300 // TestOmitEmptyBehavior validates omitempty tag behavior for required vs optional fields 301 func TestOmitEmptyBehavior(t *testing.T) { 302 t.Run("omitempty with required fields", func(t *testing.T) { 303 type TestStruct struct { 304 Required string `json:"required"` 305 Optional string `json:"optional,omitempty"` 306 } 307 308 schemaJSON := `{ 309 "type": "object", 310 "properties": { 311 "required": {"type": "string"}, 312 "optional": {"type": "string"} 313 }, 314 "required": ["required"] 315 }` 316 317 schema := compileTestSchema(t, schemaJSON) 318 319 // Should pass: required field present, optional field empty (omitted) 320 data := TestStruct{Required: "present"} 321 result := schema.Validate(data) 322 if !result.IsValid() { 323 t.Errorf("Expected validation to pass when required field is present and optional field is omitted") 324 } 325 }) 326 327 t.Run("boolean required fields with false values", func(t *testing.T) { 328 type Settings struct { 329 Email bool `json:"email"` 330 SMS bool `json:"sms"` 331 Push bool `json:"push"` 332 } 333 334 schemaJSON := `{ 335 "type": "object", 336 "properties": { 337 "email": {"type": "boolean"}, "sms": {"type": "boolean"}, "push": {"type": "boolean"} 338 }, 339 "required": ["email", "sms", "push"] 340 }` 341 342 schema := compileTestSchema(t, schemaJSON) 343 344 // false values should be valid for required boolean fields 345 settings := Settings{Email: true, SMS: false, Push: true} 346 result := schema.Validate(settings) 347 if !result.IsValid() { 348 details, _ := json.MarshalIndent(result.ToList(false), "", " ") 349 t.Errorf("Expected validation to pass for boolean fields with false values: %s", string(details)) 350 } 351 }) 352 } 353 354 // ============================================================================= 355 // Nested Structures and Arrays Tests 356 // ============================================================================= 357 358 // TestNestedStructs validates complex nested structure validation 359 func TestNestedStructs(t *testing.T) { 360 schemaJSON := `{ 361 "type": "object", 362 "properties": { 363 "id": {"type": "integer"}, "name": {"type": "string"}, "email": {"type": "string"}, 364 "age": {"type": "integer", "minimum": 0}, "is_active": {"type": "boolean"}, 365 "balance": {"type": "number"}, "tags": {"type": "array", "items": {"type": "string"}}, 366 "metadata": {"type": "object"}, "created_at": {"type": "string"}, "last_login": {"type": "string"}, 367 "preferences": { 368 "type": "object", 369 "properties": { 370 "theme": {"type": "string"}, "language": {"type": "string"}, "timezone": {"type": "string"}, 371 "notifications": { 372 "type": "object", 373 "properties": { 374 "email": {"type": "boolean"}, "sms": {"type": "boolean"}, "push": {"type": "boolean"} 375 }, 376 "required": ["email", "sms", "push"] 377 } 378 }, 379 "required": ["theme", "language", "notifications"] 380 } 381 }, 382 "required": ["id", "name", "email", "is_active", "balance", "created_at", "preferences"] 383 }` 384 385 schema := compileTestSchema(t, schemaJSON) 386 387 now := time.Now() 388 lastLogin := now.Add(-24 * time.Hour) 389 390 data := ComplexUser{ 391 ID: 1, Name: "John Doe", Email: "john@example.com", Age: intPtr(30), 392 IsActive: true, Balance: 1000.50, Tags: []string{"premium", "active"}, 393 Metadata: map[string]interface{}{"source": "web", "campaign": "summer2024"}, 394 CreatedAt: now, LastLogin: &lastLogin, 395 Preferences: UserPreferences{ 396 Theme: "dark", Language: "en", Timezone: "UTC", 397 Notifications: NotificationSettings{Email: true, SMS: false, Push: true}, 398 }, 399 } 400 401 result := schema.Validate(data) 402 if !result.IsValid() { 403 details, _ := json.MarshalIndent(result.ToList(false), "", " ") 404 t.Errorf("Expected validation to pass for complex nested structs: %s", string(details)) 405 } 406 } 407 408 // TestArrayTypes validates array and slice handling 409 func TestArrayTypes(t *testing.T) { 410 schemaJSON := `{ 411 "type": "object", 412 "properties": { 413 "string_array": {"type": "array", "items": {"type": "string"}}, 414 "int_array": {"type": "array", "items": {"type": "integer"}}, 415 "user_array": { 416 "type": "array", 417 "items": { 418 "type": "object", 419 "properties": { 420 "name": {"type": "string"}, "age": {"type": "integer"}, "sdk": {"type": "string"} 421 }, 422 "required": ["name"] 423 } 424 } 425 }, 426 "required": ["string_array", "int_array", "user_array"] 427 }` 428 429 schema := compileTestSchema(t, schemaJSON) 430 431 data := ArrayTypes{ 432 StringArray: []string{"hello", "world"}, 433 IntArray: []int{1, 2, 3, 4, 5}, 434 UserArray: []BasicUser{{Name: "Alice", Age: 25, SDK: "go"}, {Name: "Bob", Age: 30, SDK: "python"}}, 435 } 436 437 result := schema.Validate(data) 438 if !result.IsValid() { 439 details, _ := json.MarshalIndent(result.ToList(false), "", " ") 440 t.Errorf("Expected validation to pass for array types: %s", string(details)) 441 } 442 } 443 444 // ============================================================================= 445 // JSON Schema Constraint Tests 446 // ============================================================================= 447 448 // TestPropertyConstraints validates maxProperties and minProperties 449 func TestPropertyConstraints(t *testing.T) { 450 tests := []struct { 451 name string 452 schemaJSON string 453 data interface{} 454 shouldError bool 455 }{ 456 { 457 name: "maxProperties violation", 458 schemaJSON: `{"type": "object", "maxProperties": 2}`, 459 data: struct { 460 A string `json:"a"` 461 B string `json:"b"` 462 C string `json:"c"` 463 }{A: "a", B: "b", C: "c"}, 464 shouldError: true, 465 }, 466 { 467 name: "minProperties violation", 468 schemaJSON: `{"type": "object", "minProperties": 3}`, 469 data: struct { 470 A string `json:"a"` 471 B string `json:"b"` 472 }{A: "a", B: "b"}, 473 shouldError: true, 474 }, 475 { 476 name: "property count within bounds", 477 schemaJSON: `{"type": "object", "minProperties": 1, "maxProperties": 3}`, 478 data: struct { 479 A string `json:"a"` 480 B string `json:"b"` 481 }{A: "a", B: "b"}, 482 shouldError: false, 483 }, 484 } 485 486 for _, tt := range tests { 487 t.Run(tt.name, func(t *testing.T) { 488 schema := compileTestSchema(t, tt.schemaJSON) 489 result := schema.Validate(tt.data) 490 if (result.IsValid() == false) != tt.shouldError { 491 if tt.shouldError { 492 t.Errorf("Expected validation to fail for %s", tt.name) 493 } else { 494 details, _ := json.MarshalIndent(result.ToList(false), "", " ") 495 t.Errorf("Expected validation to pass for %s, but got: %s", tt.name, string(details)) 496 } 497 } 498 }) 499 } 500 } 501 502 // TestValueConstraints validates enum, const, and oneOf constraints 503 func TestValueConstraints(t *testing.T) { 504 t.Run("enum validation", func(t *testing.T) { 505 schemaJSON := `{ 506 "type": "object", 507 "properties": { 508 "status": {"type": "string", "enum": ["active", "inactive", "pending"]}, 509 "priority": {"type": "integer", "enum": [1, 2, 3, 4, 5]} 510 }, 511 "required": ["status", "priority"] 512 }` 513 514 schema := compileTestSchema(t, schemaJSON) 515 516 // Valid enum values 517 validData := struct { 518 Status string `json:"status"` 519 Priority int `json:"priority"` 520 }{Status: "active", Priority: 3} 521 522 result := schema.Validate(validData) 523 if !result.IsValid() { 524 t.Errorf("Expected validation to pass for valid enum values") 525 } 526 527 // Invalid enum values 528 invalidData := struct { 529 Status string `json:"status"` 530 Priority int `json:"priority"` 531 }{Status: "unknown", Priority: 10} 532 533 result = schema.Validate(invalidData) 534 if result.IsValid() { 535 t.Errorf("Expected validation to fail for invalid enum values") 536 } 537 }) 538 539 t.Run("const validation", func(t *testing.T) { 540 schemaJSON := `{ 541 "type": "object", 542 "properties": { 543 "version": {"const": "1.0.0"}, 544 "type": {"const": "user"} 545 }, 546 "required": ["version", "type"] 547 }` 548 549 schema := compileTestSchema(t, schemaJSON) 550 551 // Valid const values 552 validData := struct { 553 Version string `json:"version"` 554 Type string `json:"type"` 555 }{Version: "1.0.0", Type: "user"} 556 557 result := schema.Validate(validData) 558 if !result.IsValid() { 559 t.Errorf("Expected validation to pass for valid const values") 560 } 561 562 // Invalid const values 563 invalidData := struct { 564 Version string `json:"version"` 565 Type string `json:"type"` 566 }{Version: "2.0.0", Type: "admin"} 567 568 result = schema.Validate(invalidData) 569 if result.IsValid() { 570 t.Errorf("Expected validation to fail for invalid const values") 571 } 572 }) 573 574 t.Run("oneOf validation", func(t *testing.T) { 575 schemaJSON := `{ 576 "type": "object", 577 "properties": { 578 "value": { 579 "oneOf": [ 580 {"type": "string", "maxLength": 5}, 581 {"type": "integer", "minimum": 10} 582 ] 583 } 584 }, 585 "required": ["value"] 586 }` 587 588 schema := compileTestSchema(t, schemaJSON) 589 590 // Valid oneOf cases 591 stringData := struct { 592 Value string `json:"value"` 593 }{Value: "hello"} 594 intData := struct { 595 Value int `json:"value"` 596 }{Value: 15} 597 598 for _, data := range []interface{}{stringData, intData} { 599 result := schema.Validate(data) 600 if !result.IsValid() { 601 t.Errorf("Expected validation to pass for valid oneOf value") 602 } 603 } 604 }) 605 } 606 607 // ============================================================================= 608 // Advanced JSON Schema Features Tests 609 // ============================================================================= 610 611 // TestAdvancedSchemaFeatures covers patternProperties, additionalProperties, etc. 612 func TestAdvancedSchemaFeatures(t *testing.T) { 613 t.Run("patternProperties", func(t *testing.T) { 614 type TestStruct struct { 615 Foo1 string `json:"foo1"` 616 Foo2 string `json:"foo2"` 617 Bar string `json:"bar"` 618 } 619 620 schemaJSON := `{ 621 "type": "object", 622 "patternProperties": { 623 "^foo": {"type": "string", "minLength": 3} 624 }, 625 "additionalProperties": {"type": "string"} 626 }` 627 628 schema := compileTestSchema(t, schemaJSON) 629 data := TestStruct{Foo1: "hello", Foo2: "world", Bar: "ok"} 630 631 result := schema.Validate(data) 632 if !result.IsValid() { 633 details, _ := json.MarshalIndent(result.ToList(false), "", " ") 634 t.Errorf("Expected validation to pass for patternProperties: %s", string(details)) 635 } 636 }) 637 638 t.Run("additionalProperties false", func(t *testing.T) { 639 type TestStruct struct { 640 Name string `json:"name"` 641 Age int `json:"age"` 642 } 643 644 schemaJSON := `{ 645 "type": "object", 646 "properties": {"name": {"type": "string"}}, 647 "additionalProperties": false 648 }` 649 650 schema := compileTestSchema(t, schemaJSON) 651 data := TestStruct{Name: "Alice", Age: 30} // Age should cause failure 652 653 result := schema.Validate(data) 654 if result.IsValid() { 655 t.Error("Expected validation to fail for additionalProperties: false") 656 } 657 }) 658 659 t.Run("propertyNames validation", func(t *testing.T) { 660 type TestStruct struct { 661 ValidName string `json:"aa"` 662 InvalidName string `json:"bbb"` // Length 3, should fail maxLength: 2 663 } 664 665 schemaJSON := `{ 666 "type": "object", 667 "propertyNames": {"maxLength": 2} 668 }` 669 670 schema := compileTestSchema(t, schemaJSON) 671 data := TestStruct{ValidName: "valid", InvalidName: "invalid"} 672 673 result := schema.Validate(data) 674 if result.IsValid() { 675 t.Error("Expected validation to fail for propertyNames constraint") 676 } 677 }) 678 679 t.Run("dependentRequired", func(t *testing.T) { 680 type TestStruct struct { 681 Name string `json:"name,omitempty"` 682 FirstName string `json:"firstName,omitempty"` 683 LastName string `json:"lastName,omitempty"` 684 } 685 686 schemaJSON := `{ 687 "type": "object", 688 "properties": { 689 "name": {"type": "string"}, "firstName": {"type": "string"}, "lastName": {"type": "string"} 690 }, 691 "dependentRequired": {"name": ["firstName", "lastName"]} 692 }` 693 694 schema := compileTestSchema(t, schemaJSON) 695 696 // Should fail: has name but missing dependent fields 697 failData := TestStruct{Name: "John"} 698 result := schema.Validate(failData) 699 if result.IsValid() { 700 t.Error("Expected validation to fail when dependent required fields are missing") 701 } 702 703 // Should pass: has name with all dependent fields 704 passData := TestStruct{Name: "John", FirstName: "John", LastName: "Doe"} 705 result = schema.Validate(passData) 706 if !result.IsValid() { 707 t.Error("Expected validation to pass when all dependent properties are present") 708 } 709 710 // Should pass: no dependent property present 711 emptyData := TestStruct{} 712 result = schema.Validate(emptyData) 713 if !result.IsValid() { 714 t.Error("Expected validation to pass when dependent property is not present") 715 } 716 }) 717 } 718 719 // ============================================================================= 720 // Edge Cases and Compatibility Tests 721 // ============================================================================= 722 723 // TestEdgeCases covers boundary conditions and special cases 724 func TestEdgeCases(t *testing.T) { 725 tests := []struct { 726 name string 727 schemaJSON string 728 data interface{} 729 shouldPass bool 730 }{ 731 { 732 name: "empty struct", 733 schemaJSON: `{"type": "object"}`, 734 data: EmptyStruct{}, 735 shouldPass: true, 736 }, 737 { 738 name: "nil pointer to struct (as null)", 739 schemaJSON: `{"oneOf": [{"type": "object"}, {"type": "null"}]}`, 740 data: (*BasicUser)(nil), 741 shouldPass: true, 742 }, 743 { 744 name: "single field struct", 745 schemaJSON: `{"type": "object", "properties": {"value": {"type": "string"}}, "required": ["value"]}`, 746 data: SingleField{Value: "test"}, 747 shouldPass: true, 748 }, 749 { 750 name: "pointer to struct", 751 schemaJSON: `{"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]}`, 752 data: &BasicUser{Name: "Alice"}, 753 shouldPass: true, 754 }, 755 { 756 name: "double pointer to struct", 757 schemaJSON: `{"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]}`, 758 data: func() **BasicUser { u := &BasicUser{Name: "Alice"}; return &u }(), 759 shouldPass: true, 760 }, 761 } 762 763 for _, tt := range tests { 764 t.Run(tt.name, func(t *testing.T) { 765 schema := compileTestSchema(t, tt.schemaJSON) 766 result := schema.Validate(tt.data) 767 if result.IsValid() != tt.shouldPass { 768 if tt.shouldPass { 769 details, _ := json.MarshalIndent(result.ToList(false), "", " ") 770 t.Errorf("Expected validation to pass for %s: %s", tt.name, string(details)) 771 } else { 772 t.Errorf("Expected validation to fail for %s", tt.name) 773 } 774 } 775 }) 776 } 777 } 778 779 // TestBackwardCompatibility ensures existing map validation still works 780 func TestBackwardCompatibility(t *testing.T) { 781 schemaJSON := `{ 782 "type": "object", 783 "properties": {"name": {"type": "string"}}, 784 "required": ["name"] 785 }` 786 787 schema := compileTestSchema(t, schemaJSON) 788 789 t.Run("original map validation", func(t *testing.T) { 790 data := map[string]interface{}{"name": "test"} 791 result := schema.Validate(data) 792 if !result.IsValid() { 793 t.Error("Original map validation should still work") 794 } 795 }) 796 797 t.Run("mixed validation in same schema", func(t *testing.T) { 798 // Test both map and struct with same schema 799 mapData := map[string]interface{}{"name": "map test"} 800 structData := struct { 801 Name string `json:"name"` 802 }{Name: "struct test"} 803 804 mapResult := schema.Validate(mapData) 805 structResult := schema.Validate(structData) 806 807 if !mapResult.IsValid() || !structResult.IsValid() { 808 t.Error("Both map and struct validation should work with same schema") 809 } 810 }) 811 } 812 813 // ============================================================================= 814 // Performance Benchmarks 815 // ============================================================================= 816 817 // BenchmarkStructValidation measures struct validation performance 818 func BenchmarkStructValidation(b *testing.B) { 819 schemaJSON := `{ 820 "type": "object", 821 "properties": { 822 "name": {"type": "string"}, "age": {"type": "integer"}, "email": {"type": "string"} 823 }, 824 "required": ["name", "email"] 825 }` 826 827 compiler := NewCompiler() 828 schema, _ := compiler.Compile([]byte(schemaJSON)) 829 830 user := struct { 831 Name string `json:"name"` 832 Age int `json:"age"` 833 Email string `json:"email"` 834 }{Name: "John Doe", Age: 30, Email: "john@example.com"} 835 836 b.ResetTimer() 837 for i := 0; i < b.N; i++ { 838 _ = schema.Validate(user) 839 } 840 } 841 842 // BenchmarkMapValidation measures map validation performance for comparison 843 func BenchmarkMapValidation(b *testing.B) { 844 schemaJSON := `{ 845 "type": "object", 846 "properties": { 847 "name": {"type": "string"}, "age": {"type": "integer"}, "email": {"type": "string"} 848 }, 849 "required": ["name", "email"] 850 }` 851 852 compiler := NewCompiler() 853 schema, _ := compiler.Compile([]byte(schemaJSON)) 854 855 data := map[string]interface{}{ 856 "name": "John Doe", "age": 30, "email": "john@example.com", 857 } 858 859 b.ResetTimer() 860 for i := 0; i < b.N; i++ { 861 _ = schema.Validate(data) 862 } 863 }