samchon / typia

Super-fast/easy runtime validations and serializations through transformation
https://typia.io/
MIT License
4.29k stars 148 forks source link

add UniqueItems tag #1062

Open xxmichas opened 1 month ago

xxmichas commented 1 month ago

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;
}
samchon commented 1 month ago

Do you know what uniqueItems work when the element type is object or another array?

xxmichas commented 1 month ago

Do you know what uniqueItems work 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": "user1@example.com",
        "age": 25
      }
    },
    {
      "userId": 2,
      "username": "user2",
      "profile": {
        "email": "user2@example.com",
        "age": 30
      }
    }
  ]
}

Invalid

{
  "users": [
    {
      "userId": 1,
      "username": "user1",
      "profile": {
        "email": "user1@example.com",
        "age": 25
      }
    },
    {
      "userId": 1,
      "username": "user1",
      "profile": {
        "email": "user1@example.com",
        "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]
  ]
}
xxmichas commented 1 month ago

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.

samchon commented 1 month ago

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[].

xxmichas commented 1 month ago

All good. Thank you for your amazing libraries.