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  }