Skip to content
Guides

5 JSON Schema uniqueItems Tests Before API Release

·9 min read
5 JSON Schema uniqueItems Tests Before API Release - Guides guide from JSON Prettifier

Quick answer: JSON Schema uniqueItems rejects repeated complete values in an array. It does not mean “unique by ID.” These five tests separate value equality, nested structures, business identity, and request limits so a valid-looking batch cannot silently bypass the rule your API actually needs.

A batch containing two customer records can pass schema validation even when both records name the same customer. That is not necessarily a validator bug: the contract may be asking the wrong question. Use these fixtures before deciding whether an array is a set of values or a list of entities.

This guide targets Draft 2020-12 behavior explicitly, rather than assuming every validator implements the newest specification work. For background, Wikipedia’s JSON overview explains the data format and its schema ecosystem. The technical rules below come from the specification and executable fixtures, not that background summary.

1. Does the schema reject repeated scalar values without rejecting different types?

A uniqueItems schema should reject an array containing the same scalar value twice, while preserving distinctions between different JSON types. Test repeated strings, repeated numbers, and repeated null values separately. Also test a number beside its string representation, because business normalization and JSON value equality are different decisions.

Start with the smallest possible contract. If schema vocabulary is new, begin with the JSON Schema introduction, then return to these focused fixtures. The official array reference documents uniqueness alongside item and length restrictions. Keep those concerns separate while diagnosing a failure: otherwise an item-type error can disguise a broken uniqueness implementation.

{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"array","uniqueItems":true}

The fixture [1,1] must fail, while [1,"1"] must pass this particular schema. Likewise, [null,null] fails and [false,0] passes. These mixed arrays are useful diagnostic fixtures even when your production contract later restricts every item to a string. They reveal accidental coercion in wrappers before it reaches real input.

Do not infer uniqueness from the spelling of numeric tokens. After parsing, 1 and 1.0 represent the same numeric value for schema comparison. A test that expects them to be distinct is testing text preservation rather than the JSON data model. Keep raw-byte comparisons in a different suite when a signature or archive requires them.

For production tags, add items with type: "string". That restriction stops numbers entering the tag list, but it still does not make "Blue" equal to "blue". If the product treats those labels as identical, define normalization before validation and test the normalized result. Avoid silently changing case when identifiers are case-sensitive.

Write the expected result beside each fixture. Include a clear failure message identifying the array location, while avoiding dependence on a vendor’s exact English wording. A version upgrade can change error phrasing without changing correctness. Assert the validity result and relevant keyword or path when your validator exposes them reliably.

2. Are equal objects rejected even when their property order changes?

Objects with the same properties and equal corresponding values are duplicates regardless of property order. Test two separately parsed objects whose keys appear in different orders. A schema validator must compare JSON values, whereas a JavaScript Set compares object references and a serialized-string shortcut can preserve irrelevant key ordering.

The Draft 2020-12 core specification defines instance equality. That definition is the important boundary: object membership is unordered, while corresponding values still matter. A customer with a different email is a different complete object even if the customer ID matches.

[
  {"id":"c1","active":true},
  {"active":true,"id":"c1"}
]

This array must fail the basic uniqueness schema. Both elements have the same two properties and values. Moving active before id does not create a new record. A validator that accepts this fixture needs investigation before it becomes part of your release gate.

A quick implementation using new Set(records) will not catch these duplicates because the two objects occupy different references in memory. A shortcut using new Set(records.map(JSON.stringify)) can also miss them when property order differs. Neither shortcut implements the complete schema equality contract for arbitrary JSON values.

Use the JSON comparison tool to inspect the fixture’s fields while debugging. The tool helps you examine differences; it does not execute a JSON Schema or certify uniqueness. For the separate issue of stable serialized output, the JSON key-order test guide explains why bytes and semantic equality need different assertions.

Add another fixture with an extra property on just one object. That array should pass the basic uniqueness assertion because the complete objects differ. It can still fail your production item schema if additional fields are forbidden. Record both expectations instead of treating a single “invalid” result as proof that uniqueness worked.

3. Does nested equality preserve array order and repeated null values?

Nested objects and arrays need recursive value comparison, not a shallow scan of top-level properties. Equal nested arrays are duplicates, but arrays containing the same values in a different order are different JSON values. Include repeated null values inside structures so a truthiness check cannot accidentally skip them during comparison.

These fixtures separate three behaviors that are easy to conflate. The top-level array is the collection being checked. Each nested array is itself one value. Object property order is irrelevant to equality, but the order of elements inside each nested array remains significant.

[["read","write"],["write","read"]]

That array passes uniqueItems: true. Replacing the second element with ["read","write"] makes it fail. This is different from a business rule declaring that permission sets are unordered. If permissions are sets, normalize each permission list under a documented policy before comparing complete records, or validate that business rule separately.

Next, test [{"v":null},{"v":null}] and [{"v":null},{}]. The first fails uniqueness; the second passes because a missing property differs from a property whose value is null. A production schema can require v, but that is a separate constraint. The null versus undefined guide provides more context for application boundaries.

Recursive fixtures also expose partial comparison helpers. A helper that only compares top-level keys could incorrectly treat two different nested values as equal. A helper that only checks reference identity could miss two deeply equal structures. Include both a nested duplicate and a nested near-match so the suite catches false rejection as well as false acceptance.

Keep examples within ordinary JSON values. Functions, undefined, class instances, and circular references belong to language-specific conversion tests before JSON validation. Once input has been parsed as JSON, the validator should receive the JSON data model, with parser limits and number precision considered separately. Do not describe host-language object behavior as a schema guarantee.

4. Can two different records with the same business ID still pass?

Two objects sharing a business ID can pass uniqueItems when another property differs, because the keyword compares complete elements. Test that acceptance explicitly, then add a separate check for repeated IDs. Require and type the ID first, and define whether case, whitespace, or numeric representations affect business identity.

The portable uniqueItems assertion has a boolean value. It does not accept a property path selecting an identity field. Do not publish a schema containing a made-up uniqueBy keyword and assume other validators enforce it; extensions require explicit implementation agreement.

[{"id":"c1","email":"first@example.com"},{"id":"c1","email":"second@example.com"}]

This fixture passes complete-value uniqueness. If the endpoint promises one update per customer, application validation must reject the repeated id. The following helper assumes the item schema has already required a string ID. It deliberately performs no trimming or case folding, so the identity rule remains visible.

function hasUniqueIds(records) {
  const seen = new Set();
  for (const record of records) {
    if (seen.has(record.id)) return false;
    seen.add(record.id);
  }
  return true;
}

Test the helper with equal IDs and different emails, different IDs and equal emails, and an empty string if your item schema permits it. Usually a customer identifier should have a nonempty constraint, but that decision belongs in the item contract. A missing ID should fail structural validation before this helper runs.

The distinction matters for updates. Two entries targeting the same customer can create order-dependent behavior even if their payloads differ. Rejecting conflicting targets gives clients a clear correction path. Silently choosing the first or last entry introduces a merge policy that deserves its own documentation and tests.

Payload identity also stops at the request boundary. Another request can use the same identifier after this check passes. Persistent uniqueness therefore needs a database constraint or equivalent authoritative mechanism. Keep that invariant close to storage and translate a conflict into your documented API error format, rather than promising global uniqueness from local schema validation.

5. Are empty inputs, size limits, and unchanged payloads tested separately?

Uniqueness alone permits empty arrays and does not impose a maximum length or remove duplicates. Add explicit length constraints and confirm validation leaves the submitted payload unchanged. Test boundary sizes and representative nested records on your actual validator, because an accepted schema says nothing about your service’s operational capacity.

Use minItems for a required batch and maxItems for a documented upper bound. A sample endpoint might permit one through one hundred records; that is an example contract, not a universal recommendation. Choose the real limit using your processing cost, response policy, and measured behavior under representative traffic.

{"type":"array","minItems":1,"maxItems":100,"uniqueItems":true}

Test zero, one, one hundred, and one hundred one distinct elements. Also place a duplicate at the beginning and end of an otherwise distinct batch. The latter fixture makes sure the integration checks the entire collection rather than validating only the first page or truncating input before checking it.

Validation should not silently deduplicate a request. If a library offers data-modifying options, configure and test them deliberately, then document the behavior outside the meaning of uniqueItems. Take a deep copy of the JSON fixture before validation and compare it afterward. A successful boolean result is insufficient evidence that the original request survived unchanged.

Measure resource use with the validator and payload shapes you deploy. Deeply nested records cost more to compare than short identifiers, and implementations can use different algorithms. Avoid attaching a universal complexity or throughput claim to the keyword. Transport byte limits, parser depth limits, and batch-length limits protect different stages and should each have their own failure response.

For an executable cross-check, Python’s jsonschema package exposes Draft202012Validator. Install it in an isolated environment, record the installed version with your test results, and run the cases below. Keep your deployment validator’s own suite too; agreement between two tools is useful evidence, not a substitute for the contract.

from jsonschema import Draft202012Validator
v = Draft202012Validator({"type": "array", "uniqueItems": True})
cases = [
    ([1, 1], False), ([1, "1"], True),
    ([None, None], False), ([False, 0], True),
    ([{"a": 1, "b": 2}, {"b": 2, "a": 1}], False),
    ([[1, 2], [2, 1]], True), ([[1, 2], [1, 2]], False),
    ([{"v": None}, {}], True),
    ([{"id": "c1", "x": 1}, {"id": "c1", "x": 2}], True),
    ([], True),
]
for instance, expected in cases:
    assert v.is_valid(instance) is expected

Before merging, save the fixture results alongside your normal API checks. Review failures by constraint instead of merely counting invalid requests. That makes the release decision concrete: the schema rejects duplicate values, the application rejects conflicting identities, and storage protects identity across requests.

Frequently Asked Questions

Does uniqueItems make an object property unique?
No. JSON Schema uniqueItems compares complete array elements, including every property of an object. Two records can share an ID and still be different elements because another property differs. Enforce uniqueness of a selected business field with application validation or a deliberately supported extension, and enforce stored identity with database constraints.
Does uniqueItems reject an empty array?
No. An empty array has no repeated elements, so it satisfies uniqueItems. Add minItems when the endpoint requires at least one element. Keep that requirement separate from uniqueness, because a request can satisfy one constraint while violating the other, and clients need an accurate explanation of the failure.
Are objects with different key orders duplicates?
Yes, when their property names and corresponding values are equal. JSON Schema instance equality does not depend on object property order. A raw JSON.stringify comparison can disagree because serialization order is observable in JavaScript. Use a conforming schema validator for schema equality rather than comparing serialized strings.
Can uniqueItems remove duplicates automatically?
No. The keyword is a validation assertion, not a transformation instruction. A conforming validation result reports whether the instance satisfies the constraint. If an application offers deduplication, document it as a separate operation with explicit rules for which record survives, and validate the resulting data again before use.
Should an API use uniqueItems for database uniqueness?
Use uniqueItems for duplicates within a submitted array when complete-value equality matches the contract. It cannot inspect other requests or existing database rows. Protect persistent identifiers with a database uniqueness constraint and handle conflicts in the API, including concurrent requests that independently pass all local payload checks.

Working with JSON right now?

Paste it into JSON Prettifier to format, validate, or minify it. Everything runs in your browser.

Open JSON Prettifier