add UniqueItems tag
A description of the problem you're trying to solve:
Currently, when defining arrays of primitive types, there is no built-in mechanism to enforce that array items are unique.
An overview of the suggested solution:
I propose adding a new tag for unique items in arrays, leveraging the uniqueItems property from the OpenAPI specification.
OpenAPI documentation on uniqueItems: Swagger Data Types - uniqueItems
I'm unsure if uniqueItems should work on object-types (or if it's even possible to implement). Name could be adjusted to indicate that it only works on primitive types (if implementing it for object types is too hard).
Code examples showing the expected behavior:
type UniqueItems = typia.tags.TagBase<{
kind: "uniqueItems";
target: "array";
value: undefined;
validate: `(new Set($input)).size === $input.length`;
exclusive: true;
schema: {
uniqueItems: true;
};
}>;
Examples of how the suggestion would work in various places:
export interface MyType {
emails: Array<string & tags.Format<"email">> & tags.MaxItems<5> & tags.UniqueItems;
}
Do you know what uniqueItems work when the element type is object or another array?
Do you know what
uniqueItemswork when the element type is object or another array?
I did some research, and indeed it seems that uniqueItems should validate the array items deeply (both nested objects and arrays).
Unfortunately spec is very minimalistic about uniqueItems, but one of its authors (Henry Andrews) suggested using this tag here to validate an array of objects.
I tested the following schemas using Ajv and it did validate deeply for all of them. Ajv's docs.
You can use this online playground to quickly test them out.
Array of objects
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"users": {
"type": "array",
"items": {
"type": "object",
"properties": {
"userId": {
"type": "integer"
},
"username": {
"type": "string"
}
},
"required": ["userId", "username"],
"additionalProperties": false
},
"uniqueItems": true
}
},
"required": ["users"]
}
Valid
{
"users": [
{
"userId": 1,
"username": "user1"
},
{
"userId": 2,
"username": "user2"
}
]
}
Invalid
{
"users": [
{
"userId": 1,
"username": "user1"
},
{
"userId": 1,
"username": "user1"
}
]
}
Array of objects - nested
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"users": {
"type": "array",
"items": {
"type": "object",
"properties": {
"userId": {
"type": "integer"
},
"username": {
"type": "string"
},
"profile": {
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email"
},
"age": {
"type": "integer",
"minimum": 0
}
},
"required": ["email", "age"],
"additionalProperties": false
}
},
"required": ["userId", "username", "profile"],
"additionalProperties": false
},
"uniqueItems": true
}
},
"required": ["users"]
}
Valid
{
"users": [
{
"userId": 1,
"username": "user1",
"profile": {
"email": "[email protected]",
"age": 25
}
},
{
"userId": 2,
"username": "user2",
"profile": {
"email": "[email protected]",
"age": 30
}
}
]
}
Invalid
{
"users": [
{
"userId": 1,
"username": "user1",
"profile": {
"email": "[email protected]",
"age": 25
}
},
{
"userId": 1,
"username": "user1",
"profile": {
"email": "[email protected]",
"age": 25
}
}
]
}
Matrix
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"matrix": {
"type": "array",
"items": {
"type": "array",
"items": {
"type": "integer"
},
"uniqueItems": true
},
"uniqueItems": true
}
},
"required": ["matrix"]
}
Valid
{
"matrix": [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
}
Invalid
{
"matrix": [
[1, 2, 3],
[1, 2, 3],
[4, 5, 6]
]
}
I also noticed a small issue with the current uniqueItems implementation. When false is passed into tags.UniqueItems, empty arrays and arrays with only one item are considered invalid, even though they should be valid.
Nested object and array case, I need to make special function for them.
By the way, using external function in the type tag is not possible now. It would be supported at v7 update, so that please wait for some months about that feature. Until that, just hope to satisfy only with atomic value unique checking like string[].
All good. Thank you for your amazing libraries.
Yes, ideally any types should be supported, that is, the user will be required to create a comparison function. For primitives, you can use something like this by default:
arr.filter((v, i, a) => a.indexOf(v) === i)
new Set(arr).size === arr.length
It would also be nice to create a comparator automatically based on types.
Typia v7 has completed this issue. It even distinguishes nested object or array types.