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  }