github.com/kaptinlin/jsonschema@v0.4.6/uniqueItems.go (about) 1 package jsonschema 2 3 import ( 4 "fmt" 5 "strings" 6 7 "github.com/goccy/go-json" 8 ) 9 10 // EvaluateUniqueItems checks if all elements in the array are unique when the "uniqueItems" property is set to true. 11 // According to the JSON Schema Draft 2020-12: 12 // - If "uniqueItems" is false, the data always validates successfully. 13 // - If "uniqueItems" is true, the data validates successfully only if all elements in the array are unique. 14 // 15 // This function only applies when the data is an array and "uniqueItems" is true. 16 // 17 // This method ensures that the array elements conform to the uniqueness constraints defined in the schema. 18 // If the uniqueness constraint is violated, it returns a EvaluationError detailing the issue. 19 // 20 // Reference: https://json-schema.org/draft/2020-12/json-schema-validation#name-uniqueitems 21 func evaluateUniqueItems(schema *Schema, data []interface{}) *EvaluationError { 22 // If uniqueItems is false or not set, no validation is needed 23 if schema.UniqueItems == nil || !*schema.UniqueItems { 24 return nil 25 } 26 27 // Determine the array length to validate 28 maxLength := len(data) 29 30 // If items is false, only validate items defined by prefixItems 31 if schema.Items != nil && schema.Items.Boolean != nil && !*schema.Items.Boolean { 32 if schema.PrefixItems != nil { 33 maxLength = len(schema.PrefixItems) 34 if maxLength > len(data) { 35 maxLength = len(data) 36 } 37 } else { 38 maxLength = 0 39 } 40 } 41 42 // If there are no items to validate, return immediately 43 if maxLength == 0 { 44 return nil 45 } 46 47 // Use a map to track the index of each item 48 seen := make(map[string][]int) 49 for index, item := range data[:maxLength] { 50 itemBytes, err := json.Marshal(item) 51 if err != nil { 52 return NewEvaluationError("uniqueItems", "item_serialization_error", "Error serializing item at index {index}", map[string]interface{}{ 53 "index": fmt.Sprint(index), 54 }) 55 } 56 // Normalize JSON string to ensure same values have the same string representation 57 var normalizedItem interface{} 58 if err := json.Unmarshal(itemBytes, &normalizedItem); err != nil { 59 return NewEvaluationError("uniqueItems", "item_normalization_error", "Error normalizing item at index {index}", map[string]interface{}{ 60 "index": fmt.Sprint(index), 61 }) 62 } 63 normalizedBytes, err := json.Marshal(normalizedItem) 64 if err != nil { 65 return NewEvaluationError("uniqueItems", "item_serialization_error", "Error serializing normalized item at index {index}", map[string]interface{}{ 66 "index": fmt.Sprint(index), 67 }) 68 } 69 itemKey := string(normalizedBytes) 70 seen[itemKey] = append(seen[itemKey], index) 71 } 72 73 // Prepare to report all duplicate item positions 74 var duplicates []string 75 for _, indices := range seen { 76 if len(indices) > 1 { 77 // Convert to 1-based indices for more user-friendly output 78 for i := range indices { 79 indices[i] += 1 80 } 81 duplicates = append(duplicates, fmt.Sprintf("(%s)", strings.Trim(strings.Join(strings.Fields(fmt.Sprint(indices)), ", "), "[]"))) 82 } 83 } 84 85 if len(duplicates) > 0 { 86 return NewEvaluationError("uniqueItems", "unique_items_mismatch", "Found duplicates at the following index groups: {duplicates}", map[string]interface{}{ 87 "duplicates": strings.Join(duplicates, ", "), 88 }) 89 } 90 return nil 91 }