github.com/kaptinlin/jsonschema@v0.4.6/struct_validation.go (about)

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