github.com/kaptinlin/jsonschema@v0.4.6/compiler_test.go (about) 1 package jsonschema 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io" 8 "strings" 9 "testing" 10 11 "github.com/stretchr/testify/assert" 12 "github.com/stretchr/testify/require" 13 ) 14 15 const ( 16 remoteSchemaURL = "https://json-schema.org/draft/2020-12/schema" 17 ) 18 19 func TestCompileWithID(t *testing.T) { 20 compiler := NewCompiler() 21 schemaJSON := createTestSchemaJSON("http://example.com/schema", map[string]string{"name": "string"}, []string{"name"}) 22 23 schema, err := compiler.Compile([]byte(schemaJSON)) 24 require.NoError(t, err, "Failed to compile schema with $id") 25 26 assert.Equal(t, "http://example.com/schema", schema.ID, "Expected $id to be 'http://example.com/schema'") 27 } 28 29 func TestGetSchema(t *testing.T) { 30 compiler := NewCompiler() 31 schemaJSON := createTestSchemaJSON("http://example.com/schema", map[string]string{"name": "string"}, []string{"name"}) 32 _, err := compiler.Compile([]byte(schemaJSON)) 33 require.NoError(t, err, "Failed to compile schema") 34 35 schema, err := compiler.GetSchema("http://example.com/schema") 36 require.NoError(t, err, "Failed to retrieve compiled schema") 37 38 assert.Equal(t, "http://example.com/schema", schema.ID, "Expected to retrieve schema with $id 'http://example.com/schema'") 39 } 40 41 func TestValidateRemoteSchema(t *testing.T) { 42 compiler := NewCompiler() 43 44 // Load the meta-schema 45 metaSchema, err := compiler.GetSchema(remoteSchemaURL) 46 require.NoError(t, err, "Failed to load meta-schema") 47 48 // Ensure that the schema is not nil 49 require.NotNil(t, metaSchema, "Meta-schema is nil") 50 51 // Verify the ID of the retrieved schema 52 expectedID := remoteSchemaURL 53 assert.Equal(t, expectedID, metaSchema.ID, "Expected schema with ID %s", expectedID) 54 } 55 56 func TestCompileCache(t *testing.T) { 57 compiler := NewCompiler() 58 schemaJSON := createTestSchemaJSON("http://example.com/schema", map[string]string{"name": "string"}, []string{"name"}) 59 _, err := compiler.Compile([]byte(schemaJSON)) 60 require.NoError(t, err, "Failed to compile schema") 61 62 // Attempt to compile the same schema again 63 _, err = compiler.Compile([]byte(schemaJSON)) 64 require.NoError(t, err, "Failed to compile schema a second time") 65 66 assert.Len(t, compiler.schemas, 1, "Schema should be compiled once and cached") 67 } 68 69 func TestResolveReferences(t *testing.T) { 70 compiler := NewCompiler() 71 // Assuming this schema is already compiled and cached 72 baseSchemaJSON := createTestSchemaJSON("http://example.com/base", map[string]string{"age": "integer"}, nil) 73 _, err := compiler.Compile([]byte(baseSchemaJSON)) 74 require.NoError(t, err, "Failed to compile base schema") 75 76 refSchemaJSON := `{ 77 "$id": "http://example.com/ref", 78 "type": "object", 79 "properties": { 80 "userInfo": {"$ref": "http://example.com/base"} 81 } 82 }` 83 84 _, err = compiler.Compile([]byte(refSchemaJSON)) 85 require.NoError(t, err, "Failed to resolve reference") 86 } 87 88 func TestResolveReferencesCorrectly(t *testing.T) { 89 compiler := NewCompiler() 90 91 // Compile and cache the base schema which will be referenced. 92 baseSchemaJSON := `{ 93 "$id": "http://example.com/base", 94 "type": "object", 95 "properties": { 96 "age": {"type": "integer"} 97 }, 98 "required": ["age"] 99 }` 100 baseSchema, err := compiler.Compile([]byte(baseSchemaJSON)) 101 require.NoError(t, err, "Failed to compile base schema") 102 103 // Print base schema ID and check if cached correctly 104 cachedBaseSchema, cacheErr := compiler.GetSchema("http://example.com/base") 105 require.NoError(t, cacheErr, "Base schema cache retrieval failed") 106 require.NotNil(t, cachedBaseSchema, "Base schema not cached correctly") 107 108 // Compile another schema that references the base schema. 109 refSchemaJSON := `{ 110 "$id": "http://example.com/ref", 111 "type": "object", 112 "properties": { 113 "userInfo": {"$ref": "http://example.com/base"} 114 } 115 }` 116 117 refSchema, err := compiler.Compile([]byte(refSchemaJSON)) 118 require.NoError(t, err, "Failed to compile schema with $ref") 119 120 // Verify that the $ref in refSchema is correctly resolved to the base schema. 121 require.NotNil(t, refSchema.Properties, "Properties map should not be nil") 122 123 userInfoProp, exists := (*refSchema.Properties)["userInfo"] 124 require.True(t, exists, "userInfo property should exist") 125 require.NotNil(t, userInfoProp, "userInfo property should have a non-nil Schema") 126 127 // Assert that ResolvedRef is not nil and correctly points to the base schema 128 require.NotNil(t, userInfoProp.ResolvedRef, "ResolvedRef for userInfo should not be nil") 129 assert.Same(t, baseSchema, userInfoProp.ResolvedRef, "ResolvedRef for userInfo does not match the base schema") 130 } 131 132 func TestSetDefaultBaseURI(t *testing.T) { 133 compiler := NewCompiler() 134 baseURI := "http://example.com/schemas/" 135 compiler.SetDefaultBaseURI(baseURI) 136 137 schemaJSON := createTestSchemaJSON("schema", map[string]string{"name": "string"}, []string{"name"}) 138 schema, err := compiler.Compile([]byte(schemaJSON)) 139 require.NoError(t, err, "Failed to compile schema") 140 141 expectedURI := baseURI + "schema" 142 assert.Equal(t, expectedURI, schema.uri, "Expected schema URI to be '%s'", expectedURI) 143 } 144 145 func TestSetAssertFormat(t *testing.T) { 146 compiler := NewCompiler() 147 compiler.SetAssertFormat(true) 148 149 schemaJSON := `{ 150 "type": "string", 151 "format": "email" 152 }` 153 154 schema, err := compiler.Compile([]byte(schemaJSON)) 155 require.NoError(t, err, "Failed to compile schema") 156 157 assert.True(t, compiler.AssertFormat, "Expected AssertFormat to be true") 158 159 result := schema.Validate("not-an-email") 160 assert.False(t, result.IsValid(), "Expected validation to fail for invalid email format") 161 } 162 163 func TestRegisterDecoder(t *testing.T) { 164 compiler := NewCompiler() 165 testDecoder := func(data string) ([]byte, error) { 166 return []byte(strings.ToUpper(data)), nil 167 } 168 compiler.RegisterDecoder("test", testDecoder) 169 170 _, exists := compiler.Decoders["test"] 171 assert.True(t, exists, "Expected decoder to be registered") 172 } 173 174 func TestRegisterMediaType(t *testing.T) { 175 compiler := NewCompiler() 176 testUnmarshaler := func(data []byte) (interface{}, error) { 177 return string(data), nil 178 } 179 compiler.RegisterMediaType("test/type", testUnmarshaler) 180 181 _, exists := compiler.MediaTypes["test/type"] 182 assert.True(t, exists, "Expected media type handler to be registered") 183 } 184 185 func TestRegisterLoader(t *testing.T) { 186 compiler := NewCompiler() 187 testLoader := func(url string) (io.ReadCloser, error) { 188 return io.NopCloser(strings.NewReader(`{"type": "string"}`)), nil 189 } 190 compiler.RegisterLoader("test", testLoader) 191 192 _, exists := compiler.Loaders["test"] 193 assert.True(t, exists, "Expected loader to be registered") 194 } 195 196 // createTestSchemaJSON simplifies creating JSON schema strings for testing. 197 func createTestSchemaJSON(id string, properties map[string]string, required []string) string { 198 propsStr := "" 199 for propName, propType := range properties { 200 propsStr += fmt.Sprintf(`"%s": {"type": "%s"},`, propName, propType) 201 } 202 if len(propsStr) > 0 { 203 propsStr = propsStr[:len(propsStr)-1] // Remove the trailing comma 204 } 205 206 reqStr := "[" 207 for _, req := range required { 208 reqStr += fmt.Sprintf(`"%s",`, req) 209 } 210 if len(reqStr) > 1 { 211 reqStr = reqStr[:len(reqStr)-1] // Remove the trailing comma 212 } 213 reqStr += "]" 214 215 return fmt.Sprintf(`{ 216 "$id": "%s", 217 "type": "object", 218 "properties": {%s}, 219 "required": %s 220 }`, id, propsStr, reqStr) 221 } 222 223 // TestWithEncoderJSON tests the WithEncoderJSON method of the Compiler struct. 224 func TestWithEncoderJSON(t *testing.T) { 225 compiler := NewCompiler() 226 227 // Custom JSON encoder 228 customEncoder := func(v interface{}) ([]byte, error) { 229 // Add an encoder with a custom prefix 230 defaultBytes, err := json.Marshal(v) 231 if err != nil { 232 return nil, err 233 } 234 return append([]byte("custom:"), defaultBytes...), nil 235 } 236 237 // Set the custom encoder 238 compiler.WithEncoderJSON(customEncoder) 239 240 // Test data 241 testData := map[string]string{"test": "value"} 242 243 // Use the custom encoder to encode 244 encoded, err := compiler.jsonEncoder(testData) 245 require.NoError(t, err, "Failed to encode") 246 247 // Verify the result 248 assert.True(t, strings.HasPrefix(string(encoded), "custom:"), "Expected encoded result to start with 'custom:', got: %s", string(encoded)) 249 } 250 251 func TestWithDecoderJSON(t *testing.T) { 252 compiler := NewCompiler() 253 254 // Custom JSON decoder 255 customDecoder := func(data []byte, v interface{}) error { 256 // Remove the custom prefix 257 if bytes.HasPrefix(data, []byte("custom:")) { 258 data = bytes.TrimPrefix(data, []byte("custom:")) 259 } 260 return json.Unmarshal(data, v) 261 } 262 263 // Set the custom decoder 264 compiler.WithDecoderJSON(customDecoder) 265 266 // Test data 267 inputJSON := []byte(`custom:{"test":"value"}`) 268 var result map[string]string 269 270 // Use the custom decoder to decode 271 err := compiler.jsonDecoder(inputJSON, &result) 272 require.NoError(t, err, "Failed to decode") 273 274 // Verify the result 275 expectedValue := "value" 276 assert.Equal(t, expectedValue, result["test"], "Expected decoded result to be %s", expectedValue) 277 } 278 279 // TestSchemaReferenceOrdering tests that schema references work correctly regardless 280 // of compilation order - parent schema can be compiled before referenced child schema 281 func TestSchemaReferenceOrdering(t *testing.T) { 282 compiler := NewCompiler() 283 284 childSchema := []byte(`{ 285 "$id": "http://example.com/child", 286 "type": "object", 287 "properties": { 288 "key": { "type": "string" } 289 } 290 }`) 291 292 parentSchema := []byte(`{ 293 "type": "object", 294 "properties": { 295 "child": { "$ref": "http://example.com/child" } 296 } 297 }`) 298 299 // Compile parent first, then child - this should now work correctly 300 parentCompiledSchema, err := compiler.Compile(parentSchema) 301 require.NoError(t, err, "Failed to compile parent schema") 302 303 _, err = compiler.Compile(childSchema) 304 require.NoError(t, err, "Failed to compile child schema") 305 306 // Verify that reference is now resolved 307 require.NotNil(t, parentCompiledSchema.Properties, "Properties should not be nil") 308 childProp, exists := (*parentCompiledSchema.Properties)["child"] 309 require.True(t, exists, "child property should exist") 310 require.NotNil(t, childProp.ResolvedRef, "Reference should have been resolved after child schema compilation") 311 312 // Test valid data 313 validData := map[string]interface{}{ 314 "child": map[string]interface{}{ 315 "key": "valid", 316 }, 317 } 318 result := parentCompiledSchema.Validate(validData) 319 assert.True(t, result.IsValid(), "Valid data should pass validation") 320 321 // Test invalid data - string instead of object 322 invalidData1 := map[string]interface{}{ 323 "child": "string", 324 } 325 result = parentCompiledSchema.Validate(invalidData1) 326 assert.False(t, result.IsValid(), "Invalid data (string instead of object) should fail validation") 327 328 // Test invalid data - wrong type for key 329 invalidData2 := map[string]interface{}{ 330 "child": map[string]interface{}{ 331 "key": false, 332 }, 333 } 334 result = parentCompiledSchema.Validate(invalidData2) 335 assert.False(t, result.IsValid(), "Invalid data (boolean instead of string) should fail validation") 336 } 337 338 // TestSchemaReferenceOrderingReversed tests the original working order for comparison 339 func TestSchemaReferenceOrderingReversed(t *testing.T) { 340 compiler := NewCompiler() 341 342 childSchema := []byte(`{ 343 "$id": "http://example.com/child", 344 "type": "object", 345 "properties": { 346 "key": { "type": "string" } 347 } 348 }`) 349 350 parentSchema := []byte(`{ 351 "type": "object", 352 "properties": { 353 "child": { "$ref": "http://example.com/child" } 354 } 355 }`) 356 357 // Compile child first, then parent - this should work 358 _, err := compiler.Compile(childSchema) 359 require.NoError(t, err, "Failed to compile child schema") 360 361 parentCompiledSchema, err := compiler.Compile(parentSchema) 362 require.NoError(t, err, "Failed to compile parent schema") 363 364 // Test valid data 365 validData := map[string]interface{}{ 366 "child": map[string]interface{}{ 367 "key": "valid", 368 }, 369 } 370 result := parentCompiledSchema.Validate(validData) 371 assert.True(t, result.IsValid(), "Valid data should pass validation") 372 373 // Test invalid data - string instead of object 374 invalidData1 := map[string]interface{}{ 375 "child": "string", 376 } 377 result = parentCompiledSchema.Validate(invalidData1) 378 assert.False(t, result.IsValid(), "Invalid data (string instead of object) should fail validation") 379 380 // Test invalid data - wrong type for key 381 invalidData2 := map[string]interface{}{ 382 "child": map[string]interface{}{ 383 "key": false, 384 }, 385 } 386 result = parentCompiledSchema.Validate(invalidData2) 387 assert.False(t, result.IsValid(), "Invalid data (boolean instead of string) should fail validation") 388 } 389 390 // TestCompileBatchWithCrossReferences tests that CompileBatch can handle schemas 391 // with cross-references without causing nil pointer dereference errors 392 // This test specifically addresses the fix for using s.GetCompiler() instead of s.compiler 393 func TestCompileBatchWithCrossReferences(t *testing.T) { 394 compiler := NewCompiler() 395 396 // Define schemas with cross-references 397 schemas := map[string][]byte{ 398 "person.json": []byte(`{ 399 "$id": "person.json", 400 "type": "object", 401 "properties": { 402 "name": {"type": "string"}, 403 "address": {"$ref": "address.json"}, 404 "employer": {"$ref": "company.json"} 405 }, 406 "required": ["name"] 407 }`), 408 "address.json": []byte(`{ 409 "$id": "address.json", 410 "type": "object", 411 "properties": { 412 "street": {"type": "string"}, 413 "city": {"type": "string"}, 414 "country": {"$ref": "country.json"} 415 }, 416 "required": ["street", "city"] 417 }`), 418 "company.json": []byte(`{ 419 "$id": "company.json", 420 "type": "object", 421 "properties": { 422 "name": {"type": "string"}, 423 "address": {"$ref": "address.json"} 424 }, 425 "required": ["name"] 426 }`), 427 "country.json": []byte(`{ 428 "$id": "country.json", 429 "type": "object", 430 "properties": { 431 "name": {"type": "string"}, 432 "code": {"type": "string"} 433 }, 434 "required": ["name", "code"] 435 }`), 436 } 437 438 // CompileBatch should not panic with cross-references 439 compiledSchemas, err := compiler.CompileBatch(schemas) 440 require.NoError(t, err, "CompileBatch should not fail with cross-references") 441 require.Len(t, compiledSchemas, 4, "All schemas should be compiled") 442 443 // Test that all schemas are properly compiled 444 for schemaID, schema := range compiledSchemas { 445 assert.NotNil(t, schema, "Schema %s should not be nil", schemaID) 446 assert.Equal(t, schemaID, schema.ID, "Schema ID should match: %s", schemaID) 447 } 448 449 // Test validation with the compiled schemas 450 personSchema := compiledSchemas["person.json"] 451 require.NotNil(t, personSchema, "Person schema should be available") 452 453 // Valid test data 454 validData := map[string]interface{}{ 455 "name": "John Doe", 456 "address": map[string]interface{}{ 457 "street": "123 Main St", 458 "city": "Anytown", 459 "country": map[string]interface{}{ 460 "name": "United States", 461 "code": "US", 462 }, 463 }, 464 "employer": map[string]interface{}{ 465 "name": "Acme Corp", 466 "address": map[string]interface{}{ 467 "street": "456 Business Ave", 468 "city": "Corporate City", 469 "country": map[string]interface{}{ 470 "name": "United States", 471 "code": "US", 472 }, 473 }, 474 }, 475 } 476 477 result := personSchema.Validate(validData) 478 assert.True(t, result.IsValid(), "Valid data should pass validation") 479 480 // Invalid test data - missing required field 481 invalidData := map[string]interface{}{ 482 "address": map[string]interface{}{ 483 "street": "123 Main St", 484 "city": "Anytown", 485 }, 486 } 487 488 result = personSchema.Validate(invalidData) 489 assert.False(t, result.IsValid(), "Invalid data (missing required name) should fail validation") 490 } 491 492 // TestCompileBatchWithNestedReferences tests CompileBatch with deeply nested references 493 // to ensure the fix for GetCompiler() works correctly in all contexts 494 func TestCompileBatchWithNestedReferences(t *testing.T) { 495 compiler := NewCompiler() 496 497 schemas := map[string][]byte{ 498 "root.json": []byte(`{ 499 "$id": "root.json", 500 "type": "object", 501 "properties": { 502 "data": { 503 "type": "object", 504 "properties": { 505 "nested": {"$ref": "nested.json"} 506 } 507 } 508 } 509 }`), 510 "nested.json": []byte(`{ 511 "$id": "nested.json", 512 "type": "object", 513 "properties": { 514 "deep": { 515 "type": "object", 516 "properties": { 517 "reference": {"$ref": "leaf.json"} 518 } 519 } 520 } 521 }`), 522 "leaf.json": []byte(`{ 523 "$id": "leaf.json", 524 "type": "object", 525 "properties": { 526 "value": {"type": "string"} 527 }, 528 "required": ["value"] 529 }`), 530 } 531 532 // This should not panic due to nil compiler references 533 compiledSchemas, err := compiler.CompileBatch(schemas) 534 require.NoError(t, err, "CompileBatch should handle nested references") 535 require.Len(t, compiledSchemas, 3, "All schemas should be compiled") 536 537 // Test validation works through the entire reference chain 538 rootSchema := compiledSchemas["root.json"] 539 testData := map[string]interface{}{ 540 "data": map[string]interface{}{ 541 "nested": map[string]interface{}{ 542 "deep": map[string]interface{}{ 543 "reference": map[string]interface{}{ 544 "value": "test string", 545 }, 546 }, 547 }, 548 }, 549 } 550 551 result := rootSchema.Validate(testData) 552 assert.True(t, result.IsValid(), "Valid nested data should pass validation") 553 }