github.com/kaptinlin/jsonschema@v0.4.6/unmarshal_test.go (about) 1 package jsonschema 2 3 import ( 4 "testing" 5 "time" 6 7 "github.com/stretchr/testify/assert" 8 "github.com/stretchr/testify/require" 9 ) 10 11 // Test structures 12 type User struct { 13 ID int `json:"id"` 14 Name string `json:"name"` 15 Email *string `json:"email,omitempty"` 16 CreatedAt time.Time `json:"created_at"` 17 Active bool `json:"active"` 18 Score *float64 `json:"score,omitempty"` 19 } 20 21 type NestedUser struct { 22 ID int `json:"id"` 23 Name string `json:"name"` 24 Profile Profile `json:"profile"` 25 } 26 27 type Profile struct { 28 Age int `json:"age"` 29 Country string `json:"country"` 30 } 31 32 // TestUnmarshalBasicTypes tests basic unmarshaling with defaults 33 func TestUnmarshalBasicTypes(t *testing.T) { 34 schemaJSON := `{ 35 "type": "object", 36 "properties": { 37 "id": {"type": "integer"}, 38 "name": {"type": "string", "default": "Anonymous"}, 39 "email": {"type": "string"}, 40 "created_at": {"type": "string", "format": "date-time", "default": "2025-01-01T00:00:00Z"}, 41 "active": {"type": "boolean", "default": true}, 42 "score": {"type": "number"} 43 }, 44 "required": ["id"] 45 }` 46 47 compiler := NewCompiler() 48 schema, err := compiler.Compile([]byte(schemaJSON)) 49 require.NoError(t, err) 50 51 tests := []struct { 52 name string 53 input interface{} 54 expected User 55 }{ 56 { 57 name: "JSON bytes with defaults", 58 input: []byte(`{"id": 1}`), 59 expected: User{ 60 ID: 1, 61 Name: "Anonymous", 62 CreatedAt: parseTime("2025-01-01T00:00:00Z"), 63 Active: true, 64 }, 65 }, 66 { 67 name: "Map with partial data", 68 input: map[string]interface{}{ 69 "id": 2, 70 "name": "John", 71 }, 72 expected: User{ 73 ID: 2, 74 Name: "John", 75 CreatedAt: parseTime("2025-01-01T00:00:00Z"), 76 Active: true, 77 }, 78 }, 79 { 80 name: "Struct input", 81 input: struct { 82 ID int `json:"id"` 83 Name string `json:"name"` 84 }{ID: 3, Name: "Jane"}, 85 expected: User{ 86 ID: 3, 87 Name: "Jane", 88 CreatedAt: parseTime("2025-01-01T00:00:00Z"), 89 Active: true, 90 }, 91 }, 92 } 93 94 for _, tt := range tests { 95 t.Run(tt.name, func(t *testing.T) { 96 var result User 97 err := schema.Unmarshal(&result, tt.input) 98 require.NoError(t, err) 99 assert.Equal(t, tt.expected.ID, result.ID) 100 assert.Equal(t, tt.expected.Name, result.Name) 101 assert.Equal(t, tt.expected.Active, result.Active) 102 assert.True(t, tt.expected.CreatedAt.Equal(result.CreatedAt)) 103 }) 104 } 105 } 106 107 // TestUnmarshalPointerFields tests pointer field handling 108 func TestUnmarshalPointerFields(t *testing.T) { 109 schemaJSON := `{ 110 "type": "object", 111 "properties": { 112 "id": {"type": "integer"}, 113 "name": {"type": "string"}, 114 "email": {"type": "string", "default": "user@example.com"}, 115 "score": {"type": "number", "default": 100.0} 116 }, 117 "required": ["id", "name"] 118 }` 119 120 compiler := NewCompiler() 121 schema, err := compiler.Compile([]byte(schemaJSON)) 122 require.NoError(t, err) 123 124 input := `{"id": 1, "name": "John"}` 125 var result User 126 err = schema.Unmarshal(&result, []byte(input)) 127 require.NoError(t, err) 128 129 assert.Equal(t, 1, result.ID) 130 assert.Equal(t, "John", result.Name) 131 assert.NotNil(t, result.Email) 132 assert.Equal(t, "user@example.com", *result.Email) 133 assert.NotNil(t, result.Score) 134 assert.Equal(t, 100.0, *result.Score) 135 } 136 137 // TestUnmarshalNestedStructs tests nested struct unmarshaling 138 func TestUnmarshalNestedStructs(t *testing.T) { 139 schemaJSON := `{ 140 "type": "object", 141 "properties": { 142 "id": {"type": "integer"}, 143 "name": {"type": "string"}, 144 "profile": { 145 "type": "object", 146 "properties": { 147 "age": {"type": "integer", "default": 18}, 148 "country": {"type": "string", "default": "US"} 149 } 150 } 151 }, 152 "required": ["id", "name"] 153 }` 154 155 compiler := NewCompiler() 156 schema, err := compiler.Compile([]byte(schemaJSON)) 157 require.NoError(t, err) 158 159 input := `{"id": 1, "name": "John", "profile": {"age": 25}}` 160 var result NestedUser 161 err = schema.Unmarshal(&result, []byte(input)) 162 require.NoError(t, err) 163 164 assert.Equal(t, 1, result.ID) 165 assert.Equal(t, "John", result.Name) 166 assert.Equal(t, 25, result.Profile.Age) 167 assert.Equal(t, "US", result.Profile.Country) // Default applied 168 } 169 170 // TestUnmarshalToMap tests unmarshaling to map 171 func TestUnmarshalToMap(t *testing.T) { 172 schemaJSON := `{ 173 "type": "object", 174 "properties": { 175 "id": {"type": "integer"}, 176 "name": {"type": "string", "default": "Anonymous"}, 177 "active": {"type": "boolean", "default": true} 178 }, 179 "required": ["id"] 180 }` 181 182 compiler := NewCompiler() 183 schema, err := compiler.Compile([]byte(schemaJSON)) 184 require.NoError(t, err) 185 186 input := `{"id": 1}` 187 var result map[string]interface{} 188 err = schema.Unmarshal(&result, []byte(input)) 189 require.NoError(t, err) 190 191 assert.Equal(t, float64(1), result["id"]) // JSON numbers are float64 192 assert.Equal(t, "Anonymous", result["name"]) 193 assert.Equal(t, true, result["active"]) 194 } 195 196 // TestUnmarshalWithoutValidation tests that unmarshal works without validation 197 func TestUnmarshalWithoutValidation(t *testing.T) { 198 schemaJSON := `{ 199 "type": "object", 200 "properties": { 201 "id": {"type": "integer", "minimum": 1} 202 }, 203 "required": ["id"] 204 }` 205 206 compiler := NewCompiler() 207 schema, err := compiler.Compile([]byte(schemaJSON)) 208 require.NoError(t, err) 209 210 // This violates minimum constraint but unmarshal should still work 211 input := `{"id": 0}` 212 var result User 213 err = schema.Unmarshal(&result, []byte(input)) 214 require.NoError(t, err) // No error because validation is not performed 215 assert.Equal(t, 0, result.ID) 216 } 217 218 // TestSeparateValidationAndUnmarshal tests the intended workflow: validate first, then unmarshal 219 func TestSeparateValidationAndUnmarshal(t *testing.T) { 220 schemaJSON := `{ 221 "type": "object", 222 "properties": { 223 "id": {"type": "integer", "minimum": 1}, 224 "name": {"type": "string", "default": "Anonymous"} 225 }, 226 "required": ["id"] 227 }` 228 229 compiler := NewCompiler() 230 schema, err := compiler.Compile([]byte(schemaJSON)) 231 require.NoError(t, err) 232 233 tests := []struct { 234 name string 235 input string 236 shouldValidate bool 237 expectedID int 238 expectedName string 239 }{ 240 { 241 name: "valid data", 242 input: `{"id": 5}`, 243 shouldValidate: true, 244 expectedID: 5, 245 expectedName: "Anonymous", 246 }, 247 { 248 name: "invalid data (but unmarshal works)", 249 input: `{"id": 0}`, 250 shouldValidate: false, 251 expectedID: 0, 252 expectedName: "Anonymous", 253 }, 254 } 255 256 for _, tt := range tests { 257 t.Run(tt.name, func(t *testing.T) { 258 // Step 1: Validate 259 result := schema.Validate([]byte(tt.input)) 260 assert.Equal(t, tt.shouldValidate, result.IsValid()) 261 262 // Step 2: Unmarshal (works regardless of validation result) 263 var user User 264 err := schema.Unmarshal(&user, []byte(tt.input)) 265 require.NoError(t, err) 266 assert.Equal(t, tt.expectedID, user.ID) 267 assert.Equal(t, tt.expectedName, user.Name) 268 269 // Step 3: Handle based on validation result 270 if result.IsValid() { 271 // Proceed with valid data 272 assert.Equal(t, tt.expectedID, user.ID) 273 } else { 274 // Handle validation errors 275 assert.Contains(t, result.Errors, "properties") 276 } 277 }) 278 } 279 } 280 281 // TestWorkflowExample demonstrates the recommended usage pattern 282 func TestWorkflowExample(t *testing.T) { 283 schemaJSON := `{ 284 "type": "object", 285 "properties": { 286 "user_id": {"type": "integer", "minimum": 1}, 287 "email": {"type": "string", "format": "email"}, 288 "country": {"type": "string", "default": "US"}, 289 "active": {"type": "boolean", "default": true} 290 }, 291 "required": ["user_id", "email"] 292 }` 293 294 compiler := NewCompiler() 295 schema, err := compiler.Compile([]byte(schemaJSON)) 296 require.NoError(t, err) 297 298 type UserProfile struct { 299 UserID int `json:"user_id"` 300 Email string `json:"email"` 301 Country string `json:"country"` 302 Active bool `json:"active"` 303 } 304 305 input := []byte(`{"user_id": 123, "email": "user@example.com"}`) 306 307 // Recommended workflow 308 result := schema.Validate(input) 309 if result.IsValid() { 310 var profile UserProfile 311 err := schema.Unmarshal(&profile, input) 312 require.NoError(t, err) 313 314 assert.Equal(t, 123, profile.UserID) 315 assert.Equal(t, "user@example.com", profile.Email) 316 assert.Equal(t, "US", profile.Country) // Default applied 317 assert.Equal(t, true, profile.Active) // Default applied 318 } else { 319 t.Fatalf("Validation failed: %v", result.Errors) 320 } 321 } 322 323 // TestUnmarshalErrorCases tests various error conditions 324 func TestUnmarshalErrorCases(t *testing.T) { 325 schemaJSON := `{ 326 "type": "object", 327 "properties": { 328 "id": {"type": "integer"} 329 } 330 }` 331 332 compiler := NewCompiler() 333 schema, err := compiler.Compile([]byte(schemaJSON)) 334 require.NoError(t, err) 335 336 tests := []struct { 337 name string 338 dst interface{} 339 src interface{} 340 errType string 341 }{ 342 { 343 name: "nil destination", 344 dst: nil, 345 src: `{"id": 1}`, 346 errType: "destination", 347 }, 348 { 349 name: "non-pointer destination", 350 dst: User{}, 351 src: `{"id": 1}`, 352 errType: "destination", 353 }, 354 { 355 name: "nil pointer destination", 356 dst: (*User)(nil), 357 src: `{"id": 1}`, 358 errType: "destination", 359 }, 360 { 361 name: "invalid JSON source", 362 dst: &User{}, 363 src: []byte(`{invalid json}`), 364 errType: "source", 365 }, 366 } 367 368 for _, tt := range tests { 369 t.Run(tt.name, func(t *testing.T) { 370 err := schema.Unmarshal(tt.dst, tt.src) 371 require.Error(t, err) 372 373 var unmarshalErr *UnmarshalError 374 require.ErrorAs(t, err, &unmarshalErr) 375 assert.Equal(t, tt.errType, unmarshalErr.Type) 376 }) 377 } 378 } 379 380 // TestUnmarshalTimeHandling tests time parsing 381 func TestUnmarshalTimeHandling(t *testing.T) { 382 schemaJSON := `{ 383 "type": "object", 384 "properties": { 385 "id": {"type": "integer"}, 386 "created_at": {"type": "string", "format": "date-time"} 387 }, 388 "required": ["id"] 389 }` 390 391 compiler := NewCompiler() 392 schema, err := compiler.Compile([]byte(schemaJSON)) 393 require.NoError(t, err) 394 395 tests := []struct { 396 name string 397 timeString string 398 expectError bool 399 }{ 400 {"RFC3339", "2025-01-01T12:00:00Z", false}, 401 {"RFC3339Nano", "2025-01-01T12:00:00.123456789Z", false}, 402 {"Date only", "2025-01-01", false}, 403 {"Invalid format", "not-a-date", true}, 404 } 405 406 for _, tt := range tests { 407 t.Run(tt.name, func(t *testing.T) { 408 input := map[string]interface{}{ 409 "id": 1, 410 "created_at": tt.timeString, 411 } 412 413 var result User 414 err := schema.Unmarshal(&result, input) 415 416 if tt.expectError { 417 require.Error(t, err) 418 } else { 419 require.NoError(t, err) 420 assert.False(t, result.CreatedAt.IsZero()) 421 } 422 }) 423 } 424 } 425 426 // TestUnmarshalInputTypes tests various input types 427 func TestUnmarshalInputTypes(t *testing.T) { 428 schemaJSON := `{ 429 "type": "object", 430 "properties": { 431 "id": {"type": "integer"}, 432 "name": {"type": "string", "default": "Unknown"} 433 }, 434 "required": ["id"] 435 }` 436 437 compiler := NewCompiler() 438 schema, err := compiler.Compile([]byte(schemaJSON)) 439 require.NoError(t, err) 440 441 tests := []struct { 442 name string 443 input interface{} 444 expected User 445 }{ 446 { 447 name: "JSON bytes", 448 input: []byte(`{"id": 1}`), 449 expected: User{ 450 ID: 1, 451 Name: "Unknown", 452 }, 453 }, 454 { 455 name: "Map input", 456 input: map[string]interface{}{ 457 "id": 3, 458 }, 459 expected: User{ 460 ID: 3, 461 Name: "Unknown", 462 }, 463 }, 464 { 465 name: "Struct input", 466 input: struct { 467 ID int `json:"id"` 468 Name string `json:"name"` 469 }{ID: 4, Name: "Jane"}, 470 expected: User{ 471 ID: 4, 472 Name: "Jane", 473 }, 474 }, 475 } 476 477 for _, tt := range tests { 478 t.Run(tt.name, func(t *testing.T) { 479 var result User 480 err := schema.Unmarshal(&result, tt.input) 481 require.NoError(t, err) 482 assert.Equal(t, tt.expected.ID, result.ID) 483 assert.Equal(t, tt.expected.Name, result.Name) 484 }) 485 } 486 } 487 488 // TestUnmarshalNonObjectTypes tests non-object JSON types 489 func TestUnmarshalNonObjectTypes(t *testing.T) { 490 tests := []struct { 491 name string 492 schemaJSON string 493 input interface{} 494 expected interface{} 495 }{ 496 { 497 name: "Array schema with JSON bytes", 498 schemaJSON: `{"type": "array", "items": {"type": "integer"}}`, 499 input: []byte(`[1, 2, 3]`), 500 expected: []int{1, 2, 3}, 501 }, 502 { 503 name: "Number schema with JSON bytes", 504 schemaJSON: `{"type": "number", "minimum": 0}`, 505 input: []byte(`42.5`), 506 expected: 42.5, 507 }, 508 { 509 name: "Boolean schema", 510 schemaJSON: `{"type": "boolean"}`, 511 input: []byte(`true`), 512 expected: true, 513 }, 514 { 515 name: "String schema with plain string", 516 schemaJSON: `{"type": "string", "minLength": 3}`, 517 input: "hello world", 518 expected: "hello world", 519 }, 520 } 521 522 compiler := NewCompiler() 523 524 for _, tt := range tests { 525 t.Run(tt.name, func(t *testing.T) { 526 schema, err := compiler.Compile([]byte(tt.schemaJSON)) 527 require.NoError(t, err) 528 529 switch tt.expected.(type) { 530 case []int: 531 var result []int 532 err = schema.Unmarshal(&result, tt.input) 533 require.NoError(t, err) 534 assert.Equal(t, tt.expected, result) 535 case string: 536 var result string 537 err = schema.Unmarshal(&result, tt.input) 538 require.NoError(t, err) 539 assert.Equal(t, tt.expected, result) 540 case float64: 541 var result float64 542 err = schema.Unmarshal(&result, tt.input) 543 require.NoError(t, err) 544 assert.Equal(t, tt.expected, result) 545 case bool: 546 var result bool 547 err = schema.Unmarshal(&result, tt.input) 548 require.NoError(t, err) 549 assert.Equal(t, tt.expected, result) 550 } 551 }) 552 } 553 } 554 555 // TestUnmarshalDefaults tests default value application 556 func TestUnmarshalDefaults(t *testing.T) { 557 type User struct { 558 Name string `json:"name"` 559 Age int `json:"age"` 560 Country string `json:"country"` 561 Active bool `json:"active"` 562 Role string `json:"role"` 563 } 564 565 schemaJSON := `{ 566 "type": "object", 567 "properties": { 568 "name": {"type": "string"}, 569 "age": {"type": "integer", "minimum": 0}, 570 "country": {"type": "string", "default": "US"}, 571 "active": {"type": "boolean", "default": true}, 572 "role": {"type": "string", "default": "user"} 573 }, 574 "required": ["name", "age"] 575 }` 576 577 tests := []struct { 578 name string 579 src interface{} 580 expectError bool 581 expectedUser User 582 }{ 583 { 584 name: "JSON bytes with defaults", 585 src: []byte(`{"name": "John", "age": 25}`), 586 expectError: false, 587 expectedUser: User{ 588 Name: "John", 589 Age: 25, 590 Country: "US", 591 Active: true, 592 Role: "user", 593 }, 594 }, 595 { 596 name: "map with defaults", 597 src: map[string]interface{}{"name": "Jane", "age": 30, "country": "CA"}, 598 expectError: false, 599 expectedUser: User{ 600 Name: "Jane", 601 Age: 30, 602 Country: "CA", 603 Active: true, 604 Role: "user", 605 }, 606 }, 607 { 608 name: "missing required field - no error in unmarshal (validation should be done separately)", 609 src: []byte(`{"age": 25}`), 610 expectError: false, 611 expectedUser: User{ 612 Name: "", // Missing required field, but unmarshal still works 613 Age: 25, 614 Country: "US", 615 Active: true, 616 Role: "user", 617 }, 618 }, 619 } 620 621 for _, tt := range tests { 622 t.Run(tt.name, func(t *testing.T) { 623 compiler := NewCompiler() 624 schema, err := compiler.Compile([]byte(schemaJSON)) 625 require.NoError(t, err) 626 627 var result User 628 err = schema.Unmarshal(&result, tt.src) 629 630 if tt.expectError { 631 require.Error(t, err) 632 } else { 633 require.NoError(t, err) 634 assert.Equal(t, tt.expectedUser, result) 635 } 636 }) 637 } 638 } 639 640 // BenchmarkUnmarshal tests performance 641 func BenchmarkUnmarshal(b *testing.B) { 642 schemaJSON := `{ 643 "type": "object", 644 "properties": { 645 "id": {"type": "integer"}, 646 "name": {"type": "string", "default": "Test"} 647 } 648 }` 649 650 compiler := NewCompiler() 651 schema, _ := compiler.Compile([]byte(schemaJSON)) 652 input := []byte(`{"id": 1}`) 653 654 b.ResetTimer() 655 for i := 0; i < b.N; i++ { 656 var result User 657 _ = schema.Unmarshal(&result, input) 658 } 659 } 660 661 // Helper function to parse time strings for tests 662 func parseTime(timeStr string) time.Time { 663 t, err := time.Parse(time.RFC3339, timeStr) 664 if err != nil { 665 panic(err) 666 } 667 return t 668 }