---
head:
  - - meta
    - name: description
      content: Discover how to create a model with Ts.ED. The classes can be used as a model in your application. Ts.ED uses these models to convert JSON objects to their class equivalents.
  - - meta
    - name: keywords
      content: class model decorators ts.ed express typescript node.js javascript jsonschema validation ajv swagger
---
# Model

The classes can be used as a model in your application. Ts.ED uses these models to convert JSON objects to their class
equivalents.

The classes models can be used in the following cases:

-   Data serialization and deserialization with the ([Json mapper](/docs/json-mapper.md)),
-   Data validation with [AJV](/tutorials/ajv.md) or any library compatible with [JsonSchema](https://json-schema.org/),
-   Generating documentation with [Swagger](/tutorials/swagger.md).

To create a model, Ts.ED provides decorators which will store and generate a
standard [JsonSchema](http://json-schema.org/) model.

::: warning
Validation is only available when you import `@tsed/ajv` package in your server.

```typescript
import {Configuration} from "@tsed/di";
import "@tsed/ajv";

@Configuration()
class Server {}
```

Without this package, decorators like [Email](/ai/api/specs/schema/types/decorators/common/const-email.md) won't have any effect.

See [Validation](/docs/validation) for more information. By default, Ts.ED CLI install `@tsed/ajv` module.
:::

## Example

The example below uses decorators to describe a property of the class and store metadata such as the description of the
field.

::: code-group

```ts [Decorators]
import {Default, Enum, Format, Maximum, MaxLength, Minimum, MinLength, Pattern, Required} from "@tsed/schema";

enum Categories {
  CAT1 = "cat1",
  CAT2 = "cat2"
}

export class MyModel {
  _id: string;

  @Required()
  unique: string;

  @MinLength(3)
  @MaxLength(50)
  indexed: string;

  @Minimum(0)
  @Maximum(100)
  @Default(0)
  rate: Number = 0;

  @Enum(Categories)
  // or @Enum("type1", "type2")
  category: Categories;

  @Pattern(/[a-z]/)
  pattern: String;

  @Format("date-time")
  @Default(Date.now)
  dateCreation: Date = new Date();
}
```

```ts [Functional API]
import {s} from "@tsed/schema";

enum Categories {
  CAT1 = "cat1",
  CAT2 = "cat2"
}

export const MySchema = s.object({
  unique: s.string().required(),
  indexed: s.string().minLength(3).maxLength(50),
  rate: s.number().minimum(0).maximum(100).default(0),
  category: s.enums(Categories),
  pattern: s.string().pattern(/[a-z]/),
  dateCreation: s.datetime().default(Date.now)
});

// Inferred TypeScript type from the schema (new in v8.18+)
export type MySchema = s.infer<typeof MySchema>;
```

:::

::: tip
The Model will generate a JsonSchema which can be used by modules supporting JsonSchema spec
:::

::: warning
The schema generated by Ts.ED lists only properties decorated by at least one decorator. In the previous
example, the `_id` won't be displayed in the JsonSchema. It's very important to understand that **TypeScript** only
generates metadata on properties with at least one of these decorators:

<ApiList query="status.includes('decorator') && status.includes('schema') && status.includes('input')" />

:::

Our model is now described, we can use it inside a [Controller](/ai/api/di/types/common/decorators/decorator-controller.md) as input type parameter for our methods. Ts.ED will
use the model to convert the raw data to an instance of your model.

```ts
import {BodyParams} from "@tsed/platform-params";
import {Post} from "@tsed/schema";
import {Controller} from "@tsed/di";
import {PersonModel} from "../models/PersonModel";

@Controller("/")
export class PersonsCtrl {
  @Post("/")
  save(@BodyParams() model: PersonModel): PersonModel {
    console.log(model instanceof PersonModel); // true
    return model; // will be serialized according to your annotation on PersonModel class.
  }

  // OR

  @Post("/")
  save(@BodyParams("person") model: PersonModel): PersonModel {
    console.log(model instanceof PersonModel); // true
    return model; // will be serialized according to your annotation on PersonModel class.
  }
}
```

## Primitives

Just use at least [Property](/ai/api/specs/schema/types/decorators/common/decorator-property.md) decorator any other `schema` decorator (like [Email](/ai/api/specs/schema/types/decorators/common/const-email.md)), to create a new property on a
model. Ts.ED will get the type from Typescript metadata and transform this type to a valid Json type.

::: code-group

```ts [Decorators]
import {Default, getJsonSchema, Maximum, Minimum, Property} from "@tsed/schema";

export class Model {
  _id: string; // Won't be displayed on the Json schema

  @Property()
  prop1: string; // Displayed with the right type

  @Minimum(0)
  @Maximum(100)
  @Default(0)
  prop2: number = 0;
}

console.log(getJsonSchema(Model));
```

```ts [Functional API]
import {getJsonSchema, s} from "@tsed/schema";

export const MySchema = s.object({
  prop1: s.string(),
  prop2: s.number().min(0).max(100).default(0)
});

console.log(getJsonSchema(MySchema));
```

```json [JSON Schema]
{
  "definitions": {},
  "properties": {
    "prop1": {
      "type": "string"
    },
    "prop2": {
      "default": 0,
      "maximum": 100,
      "minimum": 0,
      "type": "number"
    }
  },
  "type": "object"
}
```

:::

## Integer

The [Integer](/ai/api/specs/schema/types/decorators/common/decorator-integer.md) decorator is used to set integer type for integral numbers.

::: code-group

```ts [Decorators]
import {Integer} from "@tsed/schema";

export class Model {
  @Integer()
  prop: number;
}
```

```ts [Functional API]
import {s} from "@tsed/schema";

export const MySchema = s.object({
  prop1: s.integer()
});
```

```json [JSON Schema]
{
  "definitions": {},
  "properties": {
    "prop": {
      "type": "integer"
    }
  },
  "type": "object"
}
```

:::

## Any types

The [Any](/ai/api/specs/schema/types/decorators/common/decorator-any.md), decorator is used to set one or more types on property. Use this method when you want to set explicitly the
json type or when you use a mixed TypeScript types.

::: code-group

```ts [Decorators]
import {Any} from "@tsed/schema";

export class Model {
  @Any()
  prop1: any;

  @Any("string", "number", "boolean")
  prop2: string | number | boolean; // mixed type

  @Any(String, null)
  prop3: string | null;
}
```

```ts [Functional API]
import {s} from "@tsed/schema";

export const MySchema = s.object({
  prop1: s.any(),
  prop2: s.any(String, Number, Boolean),
  prop3: s.any(String, null)
});
```

```json [JSON Schema]
{
  "definitions": {},
  "properties": {
    "prop1": {
      "type": ["integer", "number", "string", "boolean", "array", "object", "null"]
    },
    "prop2": {
      "type": ["string", "number", "boolean"]
    },
    "prop3": {
      "type": ["string", "null"]
    }
  },
  "type": "object"
}
```

:::

From v7.75.0, when you use [Any](/ai/api/specs/schema/types/decorators/common/decorator-any.md) decorator combined with other decorators like [MinLength](/ai/api/specs/schema/types/decorators/common/const-min-length.md), [Minimum](/ai/api/specs/schema/types/decorators/common/const-minimum.md), etc.
metadata will be automatically assigned to the right
type. For example, if you add a [Minimum](/ai/api/specs/schema/types/decorators/common/const-minimum.md) decorator, it will be assigned to the number type.

```ts
import {Any} from "@tsed/schema";

class Model {
  @Any(String, Number)
  @Minimum(0)
  @MaxLength(100)
  prop: string | number;
}
```

Produce a json-schema as follows:

```json
{
  "properties": {
    "prop": {
      "allOf": [
        {
          "type": "string",
          "maxLength": 100
        },
        {
          "type": "number",
          "minimum": 0
        }
      ]
    }
  },
  "type": "object"
}
```

## References

Use [Ref](/ai/api/specs/schema/types/decorators/common/decorator-ref.md) to assign a `$ref` directly on a property schema. This works with both local references (`#/...`) and
external URLs.

::: code-group

```ts [Decorators]
import {Property, Ref, Required} from "@tsed/schema";

export class PaymentProvidersResponse {
  @Property()
  @Required()
  @Ref("https://example.com/doc/swagger.json#/components/schemas/NotificationPayloadModel")
  paymentProviders: unknown;
}
```

```ts [Functional API]
import {s} from "@tsed/schema";

export const PaymentProvidersResponseSchema = s.object({
  paymentProviders: s.$ref("https://example.com/doc/swagger.json#/components/schemas/NotificationPayloadModel").required()
});
```

```json [JSON Schema]
{
  "type": "object",
  "required": ["paymentProviders"],
  "properties": {
    "paymentProviders": {
      "$ref": "https://example.com/doc/swagger.json#/components/schemas/NotificationPayloadModel"
    }
  }
}
```

:::

::: warning External `$ref` and validation
When using external `$ref` URLs, runtime validation with `@tsed/ajv` requires AJV to resolve those schemas.

Configure `ajv.loadSchema` (or register schemas manually with AJV) so external references can be loaded during
validation.

```ts
import {Configuration} from "@tsed/di";
import "@tsed/ajv";

@Configuration({
  ajv: {
    loadSchema: async (uri: string) => {
      const response = await fetch(uri);

      if (!response.ok) {
        throw new Error(`Unable to load schema: ${uri}`);
      }

      return response.json();
    }
  }
})
export class Server {}
```

:::

## Nullable

Use the [Nullable](/ai/api/specs/schema/types/decorators/common/decorator-nullable.md) decorator to explicitly allow null as a valid value for a property while preserving the original
TypeScript type. This is the recommended and future-proof way to model nullability in your schemas and validation.

Key points:

-   Nullable controls whether null is accepted at validation/serialization time; it does not make the property optional.
    Use [Required](/ai/api/specs/schema/types/decorators/common/const-required.md)/[Optional](/ai/api/specs/schema/types/decorators/common/decorator-optional.md) for presence.
-   Prefer writing the TypeScript union type as well (for example string | null) to reflect the runtime behavior in your
    code.

::: code-group

```ts [Decorators]
import {Nullable, Required} from "@tsed/schema";
import {MyModel, MyModel2} from "./MyModel";

export class Model {
  @Required(true, null) // allow null
  @Nullable(String)
  prop2: string | null;

  // can be used with models (JsonSchema and OS3 only)
  @Nullable(MyModel, MyModel2)
  prop3: MyModel | MyModel2 | null;
}
```

```ts [Functional API]
import {s} from "@tsed/schema";
import {Model1} from "./Model1.js";
import {Model2} from "./Model2.js";

export const MySchema = s.object({
  prop2: s.string().required().nullable(),
  prop3: s.oneOf(Model1, Model2).required().nullable()
});
```

```json [JSON Schema]
{
  "properties": {
    "prop2": {
      "oneOf": [
        {
          "type": "null"
        },
        {
          "type": "string"
        }
      ]
    }
  },
  "required": ["prop2"],
  "type": "object"
}
```

:::

::: warning

Since v7.43.0, `ajv.returnsCoercedValues` is available to address the following
issue: [#2355](https://github.com/tsedio/tsed/issues/2355)
If `returnsCoercedValues` is true, AjvService will return the coerced value instead of the original value. In this case,
`@Nullable()` will be mandatory to
allow the coercion of the value to `null`.

For example if `returnsCoercedValues` is `false` (default behavior), Ts.ED will allow null value on a field without
`@Nullable()` decorator:

```typescript
class NullableModel {
  @Property()
  propString: string; // null => null

  @Property()
  propNumber: number; // null => null

  @Property()
  propBool: boolean; // null => null
}
```

AJV will not emit a validation error when the value is null because of its coercion behavior. In that case, AjvService
returns the original value, not the coerced one.
Another issue is that your TypeScript types do not reflect the actual coerced value at runtime.

Using the `returnsCoercedValues` option, AjvService will return the coerced type. In this case, our previous model will
have the following behavior:

```typescript
class NullableModel {
  @Property()
  propString: string; // null => ''

  @Property()
  propNumber: number; // null => 0

  @Property()
  propBool: boolean; // null => false
}
```

Now `@Nullable` usage is mandatory to allow `null` value on properties:

```typescript
class NullableModel {
  @Nullable(String)
  propString: string | null; // null => null

  @Nullable(Number)
  propNumber: number | null; // null => null

  @Nullable(Boolean)
  propBool: boolean | null; // null => null
}
```

:::

::: warning
`returnsCoercedValues` is true by default since v8 version of Ts.ED.
:::

## Nullable and mixed types <Badge text="7.75.0+"/>

You can use the [Nullable](/ai/api/specs/schema/types/decorators/common/decorator-nullable.md) decorator with union types:

```ts
import {Nullable} from "@tsed/schema";

class Model {
  @Nullable(String, Number)
  prop: string | number | null;
}
```

From v7.75.0, when you combine [Nullable](/ai/api/specs/schema/types/decorators/common/decorator-nullable.md) with other constraints such as [MinLength](/ai/api/specs/schema/types/decorators/common/const-min-length.md) or [Minimum](/ai/api/specs/schema/types/decorators/common/const-minimum.md), Ts.ED
automatically assigns each constraint to the appropriate branch of the union. For example, [Minimum](/ai/api/specs/schema/types/decorators/common/const-minimum.md) applies to the
number branch, while [MaxLength](/ai/api/specs/schema/types/decorators/common/const-max-length.md) applies to the string branch.

```ts
import {Nullable} from "@tsed/schema";

class Model {
  @Nullable(String, Number)
  @Minimum(0)
  @MaxLength(100)
  prop: string | number | null;
}
```

It produces the following JSON Schema:

```json
{
  "properties": {
    "prop": {
      "oneOf": [
        {
          "type": "null"
        },
        {
          "type": "string",
          "maxLength": 100
        },
        {
          "type": "number",
          "minimum": 0
        }
      ]
    }
  },
  "type": "object"
}
```

::: warning
Because `oneOf` is strict, sometimes you can have this Ajv error message:

```
"must match exactly one schema in oneOf"
```

This happens because the object being validated matches at least two schemas. To work around this, you have two options:
either avoid overlapping between the schemas by adding different constraints to each schema, or use the [AnyOf](/ai/api/specs/schema/types/decorators/common/decorator-any-of.md)
decorator as follows:

```ts
class Model {
  @AnyOf(String, Number, null)
  @Minimum(0)
  @MaxLength(100)
  prop: string | number | null;
}
```

:::

## Nullable and Array

The [Nullable](/ai/api/specs/schema/types/decorators/common/decorator-nullable.md) decorator can be used with Array types:

```ts
import {Nullable} from "@tsed/schema";

class Model {
  @Nullable(Array)
  prop: string[] | null;
}
```

This alone will not generate the correct JSON Schema. Adding [CollectionOf](/ai/api/specs/schema/types/decorators/collections/decorator-collection-of.md) is not sufficient either:

```ts
import {Nullable} from "@tsed/schema";

class Model {
  @Nullable(Array)
  @CollectionOf(String)
  prop: string[] | null;
}
```

Why? Because at runtime, TypeScript only preserves the Object constructor in this case for the property type, not the
item type. With only decorators, Ts.ED cannot infer both "array of string" and the nullability of the array itself.

Solution: use a custom schema builder to declare both the item type and the fact that the entire array can be null:

```ts
import {s} from "@tsed/schema";

class Model {
  @Schema(s.array().items(s.string()).nullable())
  prop: string[] | null;
}
```

If you have complex schema with mixed item types, use the custom schema also as following:

```ts
import {s} from "@tsed/schema";

class Model {
  @Schema(s.array().oneOf([s.string(), s.number()]).nullable())
  prop: (string | number)[] | null;
}
```

### Nullable quick reference

-   `string | null`: use `@Nullable(String)` and declare the TypeScript type as `string | null`.
-   `number | null`: use `@Nullable(Number)` and declare the type as `number | null`.
-   `boolean | null`: use `@Nullable(Boolean)` and declare the type as `boolean | null`.
-   `(string | number) | null`: use `@Nullable(String, Number)` and declare the type as `string | number | null`.
-   `string[] | null` (nullable array itself): use the Schema builder `@Schema(s.array().items(s.string()).nullable())`.
-   `(string | number)[] | null` (nullable array itself with mixed items):
    `@Schema(s.array().oneOf([s.string(), s.number()]).nullable())`.
-   Array with nullable items (e.g., (string | null)\[]): `@Schema(s.array().items(s.string().nullable()))`.

> Note: Nullable means the value can be null. It does not mean the property can be omitted. Use [Optional](/ai/api/specs/schema/types/decorators/common/decorator-optional.md) (or remove
> [Required](/ai/api/specs/schema/types/decorators/common/const-required.md)) to make a property optional.

## Vendor extensions with `@Schema`

You can pass OpenAPI vendor extensions (`x-*`) directly to [Schema](/ai/api/specs/schema/types/decorators/common/decorator-schema.md):

```ts
import {Schema} from "@tsed/schema";

class SecretFieldModel {
  @Schema({
    type: "string",
    "x-secret": true
  })
  token: string;
}
```

The `x-*` key is preserved in generated schemas, including OpenAPI output.

Use this when the extension belongs to the schema definition itself. For other custom metadata cases, you can still use
[CustomKey](/ai/api/specs/schema/types/decorators/common/decorator-custom-key.md) / [CustomKeys](/ai/api/specs/schema/types/decorators/common/decorator-custom-keys.md).

## Regular expressions

The [Pattern](/ai/api/specs/schema/types/decorators/common/const-pattern.md) decorator is used to restrict a string to a particular regular expression. The regular expression syntax
is the one defined in JavaScript ([ECMA 262](https://www.ecma-international.org/publications/standards/Ecma-262.htm)
specifically).
See [Regular Expressions](https://json-schema.org/understanding-json-schema/reference/regular_expressions.html#regular-expressions)
for more information.

::: code-group

```ts [Decorators]
import {Pattern} from "@tsed/schema";

export class Model {
  @Pattern(/^(\\([0-9]{3}\\))?[0-9]{3}-[0-9]{4}$/)
  phone: string;
}
```

```ts [Functional API]
import {s} from "@tsed/schema";

export const MySchema = s.object({
  phone: s.string().pattern(/^(\([0-9]{3}\))?[0-9]{3}-[0-9]{4}$/)
});
```

```json [JSON Schema]
{
  "definitions": {},
  "properties": {
    "phone": {
      "pattern": "^(\\([0-9]{3}\\))?[0-9]{3}-[0-9]{4}$",
      "type": "string"
    }
  },
  "type": "object"
}
```

:::

## Format

The [Format](/ai/api/specs/schema/types/decorators/common/const-format.md) decorator allows basic semantic validation on certain kinds of string values that are commonly used. This
allows values to be constrained beyond what the other tools in JSON Schema,
including [Regular Expressions](https://json-schema.org/understanding-json-schema/reference/regular_expressions.html#regular-expressions)
can do.

::: code-group

```ts [Decorators]
import {Email, Format} from "@tsed/schema";

export class Model {
  @Email()
  email: string;

  @Format("date-time") // or @DateTime()
  dateCreation: Date = new Date();
}
```

```ts [Functional API]
import {s} from "@tsed/schema";

export const MySchema = s.object({
  email: s.email(),
  dateCreation: s.datetime() // or s.format("date-time")
});
```

```json [JSON Schema]
{
  "definitions": {},
  "properties": {
    "email": {
      "format": "email",
      "type": "string"
    },
    "dateCreation": {
      "format": "date-time",
      "type": "string"
    }
  },
  "type": "object"
}
```

:::

The following formats are supported for string validation with `format` keyword by [AJV](https://ajv.js.org/):

-   **date**: full-date according to [RFC3339](https://json-schema.org/latest/json-schema-validation.html#RFC3339).
-   **time**: time with optional time-zone.
-   **date-time**: date-time from the same source (time-zone is mandatory).
-   **uri**: full uri with optional protocol.
-   **email**: email address.
-   **hostname**: host name according to [RFC1034](https://tools.ietf.org/html/rfc1034#section-3.1).
-   **ipv4**: IP address v4.
-   **ipv6**: IP address v6.
-   **regex**: tests whether a string is a valid regular expression by passing it to RegExp constructor.

See built-in formats types
on [Jsonp-schema.org](https://json-schema.org/understanding-json-schema/reference/string.html#built-in-formats) for more
details:

## MultipleOf

Numbers can be restricted to a multiple of a given number, using the [MultipleOf](/ai/api/specs/schema/types/decorators/common/const-multiple-of.md) decorator. It may be set to any
positive number.
See [json-schema documentation](https://json-schema.org/understanding-json-schema/reference/numeric.html#multiples) for
more details.

::: code-group

```ts [Decorators]
import {MultipleOf} from "@tsed/schema";

export class Model {
  @MultipleOf(10)
  prop: number;
}
```

```ts [Functional API]
import {s} from "@tsed/schema";

export const MySchema = s.object({
  prop1: s.number().multipleOf(10)
});
```

```json [JSON Schema]
{
  "definitions": {},
  "properties": {
    "prop": {
      "multipleOf": 10,
      "type": "number"
    }
  },
  "type": "object"
}
```

:::

## Ranges

Ranges of numbers are specified using a combination of the [Minimum](/ai/api/specs/schema/types/decorators/common/const-minimum.md) and [Maximum](/ai/api/specs/schema/types/decorators/common/const-maximum.md) decorators, (or
[ExclusiveMinimum](/ai/api/specs/schema/types/decorators/common/const-exclusive-minimum.md) and [ExclusiveMaximum](/ai/api/specs/schema/types/decorators/common/const-exclusive-maximum.md) for expressing exclusive range).
See [json-schema documentation](https://json-schema.org/understanding-json-schema/reference/numeric.html#multiples) for
more details.

::: code-group

```ts [Decorators]
import {ExclusiveMaximum, Minimum} from "@tsed/schema";

export class Model {
  @Minimum(0)
  @ExclusiveMaximum(100)
  prop: number;
}
```

```ts [Functional API]
import {s} from "@tsed/schema";


export const MySchema = s.object({
  prop: s.number().min(0).exclusiveMaximum(100)
});
```

```json [JSON Schema]
{
  "definitions": {},
  "properties": {
    "prop": {
      "exclusiveMaximum": 100,
      "minimum": 0,
      "type": "number"
    }
  },
  "type": "object"
}
```

:::

## Enumerated values

The [Enum](/ai/api/specs/schema/types/decorators/common/const-enum.md) decorator is used to restrict a value to a fixed set of values. It must be an array with at least one
element, where each element is unique or a TypeScript enum.

::: code-group

```ts [Decorators]
import {Any, CollectionOf, Enum} from "@tsed/schema";

export enum Colors {
  RED = "red",
  AMBER = "amber",
  GREEN = "green"
}

export enum Days {
  MONDAY = 0,
  TUESDAY,
  WEDNESDAY,
  THURSDAY,
  FRIDAY,
  SATURDAY,
  SUNDAY
}

export class Model {
  @Enum("red", "amber", "green")
  prop1: "red" | "amber" | "green";

  @Enum(Colors)
  prop2: Colors;

  @Enum(Days)
  @CollectionOf(Number)
  prop3: Days[];

  @Enum("red", "amber", "green", null, 42)
  @Any("string", "number", "null") // in v6 not required
  prop4: string | number | null;
}
```

```ts [Functional API]
import {s} from "@tsed/schema";

export enum Colors {
  RED = "red",
  AMBER = "amber",
  GREEN = "green"
}

export enum Days {
  MONDAY = 0,
  TUESDAY,
  WEDNESDAY,
  THURSDAY,
  FRIDAY,
  SATURDAY,
  SUNDAY
}

export const MySchema = s.object({
  prop1: s.enums("red", "amber", "green"),
  prop2: s.enums(Colors),
  prop3: s.array(s.enums(Days)),
  prop4: s.enums("red", "amber", "green", null, 42).nullable()
});
```

```json [JSON Schema]
{
  "definitions": {},
  "properties": {
    "prop1": {
      "enum": ["red", "amber", "green"],
      "type": "string"
    },
    "prop2": {
      "enum": ["red", "amber", "green"],
      "type": "string"
    },
    "prop3": {
      "enum": [0, 1, 2, 3, 4, 5, 6],
      "type": "number"
    },
    "prop4": {
      "enum": ["red", "amber", "green", 42, null],
      "type": "object"
    }
  },
  "type": "object"
}
```

:::

[Enum](/ai/api/specs/schema/types/decorators/common/const-enum.md) decorator can be also in combination with [BodyParams](/ai/api/platform/platform-params/types/decorators/decorator-body-params.md) or [QueryParams](/ai/api/platform/platform-params/types/decorators/decorator-query-params.md):

```typescript
import {Enum} from "@tsed/schema";
import {Controller} from "@tsed/di";
import {QueryParams} from "@tsed/platform-params";

@Controller("/")
class MyController {
  @Post("/")
  async method(@QueryParams("type") @Enum(MyEnum) type: MyEnum): Promise<any> {
    return null;
  }
}
```

### LabelledAs <Badge text="7.85.0+"/>

The [LabelledAs](/ai/api/specs/schema/types/decorators/common/decorator-labelled-as.md) decorator is used to set a label to decorated property.

This label will be used to generate a reference for the schema related to the decorated property. This is particularly
useful when:

-   You want to create reusable schema components
-   You need to maintain consistent schema references across your API
-   You want to improve the readability of your generated swagger.json

::: code-group

```typescript [Decorators]
import {LabelledAs} from "@tsed/schema";

enum Test {
  VALUE = "VALUE",
  TEST = "TEST"
}

class Model {
  @Enum(Test)
  @MaxItems(3)
  @LabelledAs("ModelPropItem")
  prop: Test[];
}
```

```typescript [Functional API]
import {s} from "@tsed/schema";

enum Test {
  VALUE = "VALUE",
  TEST = "TEST"
}

export const MySchema = s.object({
  prop: s.array().items(s.enum(Test).label("ModelPropItem")).maxItems(3)
});
```

:::

The generated schema will be:

```json
{
  "properties": {
    "prop": {
      "type": "array",
      "maxItems": 3,
      "items": {
        "$ref": "#/components/schemas/ModelPropItem"
      }
    }
  },
  "components": {
    "schemas": {
      "ModelPropItem": {
        "type": "array",
        "items": {
          "enum": ["VALUE", "TEST"],
          "type": "string"
        },
        "maxItems": 3
      }
    }
  }
}
```

To apply the label over the array schema add extra `collection` options as second parameter:

```ts
import {LabelledAs} from "@tsed/schema";

enum Test {
  VALUE = "VALUE",
  TEST = "TEST"
}

class Model {
  @Enum(Test)
  @MaxItems(3)
  @LabelledAs("ModelPropItems", "collection")
  prop: Test[];
}
```

For Function API, you can use the `s.label()` function to set the label:

```typescript [Functional API]
import {s} from "@tsed/schema";

enum Test {
  VALUE = "VALUE",
  TEST = "TEST"
}

export const MySchema = s.object({
  prop: s.array().items(s.enum(Test)).maxItems(3).label("ModelPropItems")
});
```

The generated schema will be:

```json
{
  "properties": {
    "prop": {
      "$ref": "#/components/schemas/ModelPropItems"
    }
  },
  "components": {
    "schemas": {
      "ModelPropItems": {
        "type": "array",
        "maxItems": 3,
        "items": {
          "enum": ["VALUE", "TEST"],
          "type": "string"
        }
      }
    }
  }
}
```

### Set label to an enum <Badge text="7.17.0+"/>

With OpenSpec 3 it's now possible to create shared enums for many models in `components.schemas` instead of having their
inlined values in each model.

Ts.ED introduce a new function `enums()` to declare the enum schema as follows:

```ts
import {s} from "@tsed/schema";

enum ProductTypes {
  ALL = "ALL",
  ASSETS = "ASSETS",
  FOOD = "FOOD"
}

s.enums(ProductTypes).label("ProductTypes");

// in models
class Product {
  @Property()
  title: string;

  @Enum(ProductTypes)
  type: ProductTypes;
}

// in controller

import {Enum} from "@tsed/schema";
import {Controller} from "@tsed/di";
import {QueryParams} from "@tsed/platform-params";

@Controller("/products")
class ProductsController {
  @Get("/:type")
  @(Returns(200, Array).Of(Product))
  async get(@PathParams("type") @Enum(ProductTypes) type: ProductTypes): Promise<Product> {
    return [new Product()];
  }
}
```

## Constant values

The [Const](/ai/api/specs/schema/types/decorators/common/decorator-const.md) decorator is used to restrict a value to a single value. For example, if you only support shipping to the
United States for export reasons:

::: code-group

```ts [Decorators]
import {Const} from "@tsed/schema";

export class Model {
  @Const("United States of America")
  readonly country: string = "United States of America";
}
```

```ts [Functional API]
import {s} from "@tsed/schema";


export const MySchema = s.object({
  country: s.string().const("United States of America")
});
```

```json [JSON Schema]
{
  "definitions": {},
  "properties": {
    "country": {
      "const": "United States of America",
      "type": "string"
    }
  },
  "type": "object"
}
```

:::

## Collections

Declaring a property that uses a collection is a bit different than declaring a simple property. TypeScript stores only
the `Array`/`Set`/`Map` type when you declare the type of your property. The type used by the collection is lost.

To tell Ts.ED (and other third party which uses JsonSchema) that a property uses a collection with a specific type, you
must use [CollectionOf](/ai/api/specs/schema/types/decorators/collections/decorator-collection-of.md) decorator as follows:

::: code-group

```ts [Decorators]
import {CollectionOf, getJsonSchema} from "@tsed/schema";
import {Model} from "./primitives";
import {Role} from "./Role";
import {Security} from "./Security";

class User {
  @CollectionOf(Role)
  roles: Role[];

  @CollectionOf(Security)
  securities: Map<string, Security>;

  @CollectionOf(String)
  scopes: Set<string>;
}

console.log(getJsonSchema(Model));
```

```ts [Functional API]
import {CollectionOf, s} from "@tsed/schema";
import {Role} from "./Role";
import {Security} from "./Security";

class User {
  @CollectionOf(Role)
  roles: Role[];

  @CollectionOf(Security)
  securities: Map<string, Security>;

  @CollectionOf(String)
  scopes: Set<string>;
}

export const MySchema = s.object({
  roles: s.array(Role),
  securities: s.map(Security),
  scopes: s.set(s.string())
});
```

```json [JSON Schema]
{
  "type": "object",
  "properties": {
    "roles": {
      "type": "array",
      "items": {
        "$ref": "#/definitions/Role"
      }
    },
    "securities": {
      "type": "object",
      "additionalProperties": {
        "$ref": "#/definitions/Security"
      }
    },
    "scopes": {
      "type": "array",
      "items:": {
        "type": "string"
      }
    }
  },
  "definitions": {
    "Security": {
      "type": "object"
    },
    "Role": {
      "type": "object"
    }
  }
}
```

:::

Ts.ED provides other related collection decorators:

<ApiList query="status.includes('decorator') && status.includes('schema') && status.includes('collections')" />

## Required properties

By default, the properties defined with a decorator are not `required`. However, one can use [Required](/ai/api/specs/schema/types/decorators/common/const-required.md) decorator to
add a required property to the json schema:

::: code-group

```typescript [Decorators]
import {Required} from "@tsed/schema";

class MyModel {
  id: string;

  @Required()
  prop1: string;
}
```

```typescript [Functional API]
import {s} from "@tsed/schema";

export const MySchema = s.object({
  prop1: s.string().required()
});
```

:::

You can also add a custom ajv error message with the `.Error(msg)` function:

```typescript
import {Required} from "@tsed/schema";

class MyModel {
  id: string;

  @(Required().Error("custom message"))
  prop1: string;
}
```

> Note: Functional API doesn't support custom error message for now.

## Custom AJV error messages

If you don't like AJV's default error messages, you can customize them with these decorators:

### DefaultMsg

This is a class decorator [DefaultMsg](/ai/api/specs/schema/types/decorators/common/decorator-default-msg.md) that is used to define a default message as the name suggests:

```typescript
import {DefaultMsg} from "@tsed/schema";

@DefaultMsg("an error occurred")
class MyModel {
  id: string;

  prop1: string;
}
```

### TypeError

This is a property decorator [TypeError](/ai/api/specs/schema/types/decorators/common/decorator-type-error.md) that is used to define a custom error message for a specific type:

```typescript
import {TypeError} from "@tsed/schema";

class MyModel {
  id: string;

  @TypeError("prop1 should be a string")
  prop1: string;
}
```

### ErrorMsg

If none of the above work for you, you can use the [ErrorMsg](/ai/api/specs/schema/types/decorators/common/decorator-error-msg.md) decorator to define your own custom error message schema
using the [ajv-errors documentation](https://ajv.js.org/packages/ajv-errors.html#ajv-errors):

```typescript
import {ErrorMsg} from "@tsed/schema";

class MyModel {
  id: string;

  @ErrorMsg({type: "prop1 should be a string"})
  prop1: string;
}
```

## Additional properties

Sometimes, it can be useful to create model with additional properties. By default, Json schema is strict over extra
properties not declared in a model (
see [Properties json schema documentation](https://json-schema.org/understanding-json-schema/reference/object.html#properties)).

Use [AdditionalProperties](/ai/api/specs/schema/types/decorators/common/decorator-additional-properties.md) on your model to allow this behavior:

::: code-group

```ts [Decorators]
import {AdditionalProperties, Property} from "@tsed/schema";

@AdditionalProperties(true)
export class Model {
  @Property()
  id: string;

  [key: string]: any;
}
```

```ts [Functional API]
import {s} from "@tsed/schema";

export const MySchema = s
  .object({
    id: s.string()
  })
  .unknown(); // alias of .additionalProperties(true)
```

```json [JSON Schema]
{
  "definitions": {},
  "properties": {
    "id": {
      "type": "string"
    }
  },
  "additionalProperties": true,
  "type": "object"
}
```

:::

It is also possible to add constraint on additional properties by giving a raw JSON schema:

::: code-group

```ts [Decorators]
import {AdditionalProperties, Property, s} from "@tsed/schema";

@AdditionalProperties(s.string())
export class Model {
  @Property()
  id: string;

  [key: string]: string;
}
```

```ts [Functional API]
import {s} from "@tsed/schema";

export const MySchema = s.object({
  id: s.string()
}).additionalProperties(s.string());
```

```json [JSON Schema]
{
  "definitions": {},
  "properties": {
    "id": {
      "type": "string"
    }
  },
  "additionalProperties": {
    "type": "string"
  },
  "type": "object"
}
```

:::

Or by using [getJsonSchema](/ai/api/specs/schema/types/utils/function-get-json-schema.md) in combination with [AdditionalProperties](/ai/api/specs/schema/types/decorators/common/decorator-additional-properties.md) as follows:

::: code-group

```ts [Decorators]
import {AdditionalProperties, Property} from "@tsed/schema";

class AnotherModel {
  @Property()
  name: string;
}

@AdditionalProperties(AnotherModel)
export class Model {
  [key: string]: AnotherModel;
}
```

```ts [Functional API]
import {Property, s} from "@tsed/schema";

class AnotherModel {
  @Property()
  name: string;
}

export const MySchema = s.object({
  id: s.string()
}).additionalProperties(AnotherModel);
```

```json [JSON Schema]
{
  "definitions": {},
  "additionalProperties": {
    "definitions": {},
    "properties": {
      "name": {
        "type": "string"
      }
    },
    "type": "object"
  },
  "type": "object"
}
```

:::

## Circular ref

Circular reference can be resolved by using arrow with a [Property](/ai/api/specs/schema/types/decorators/common/decorator-property.md) and [CollectionOf](/ai/api/specs/schema/types/decorators/collections/decorator-collection-of.md) decorators:

::: code-group

```ts [Decorators]
import {CollectionOf, Property} from "@tsed/schema";

export class Photo {
  @Property(() => User)
  owner: User;
}

export class User {
  @CollectionOf(Photo)
  photos: Photo[];
}
```

```ts [Functional API]
import {s} from "@tsed/schema";

export const Photo = s.object({
  owner: s.lazyRef(() => User)
});

export const User = s.object({
  photos: s.array().items(Photo)
});
```

:::

## Custom Keys

Ts.ED introduces the [Keyword](/ai/api/specs/ajv/types/decorators/decorator-keyword.md) decorator to declare a new custom validator for Ajv. Combined with the [CustomKey](/ai/api/specs/schema/types/decorators/common/decorator-custom-key.md)
decorator to add keywords to a property of your class, you can use more complex scenarios than what basic JsonSchema
allows.

For example, we can create a custom validator to support the `range` validation over a number. To do that, we have to
define the custom validator by using [Keyword](/ai/api/specs/ajv/types/decorators/decorator-keyword.md) decorator:

```typescript
import {Keyword, KeywordMethods} from "@tsed/ajv";
import {array, number} from "@tsed/schema";

@Keyword({
  keyword: "range",
  type: "number",
  schemaType: "array",
  implements: ["exclusiveRange"],
  metaSchema: array().items([number(), number()]).minItems(2).additionalItems(false)
})
class RangeKeyword implements KeywordMethods {
  compile([min, max]: number[], parentSchema: any) {
    return parentSchema.exclusiveRange === true ? (data: any) => data > min && data < max : (data: any) => data >= min && data <= max;
  }
}
```

Then we can declare a model using the standard decorators from `@tsed/schema`:

::: code-group

```typescript [Decorators]
import {CustomKey} from "@tsed/schema";

// Custom decorator
export function Range(min: number, max: number) {
  return CustomKey("range", [min, max]);
}

// Custom Decorator
export function ExclusiveRange(bool: boolean) {
  return CustomKey("exclusiveRange", bool);
}

export class Product {
  @CustomKey("range", [10, 100])
  @CustomKey("exclusiveRange", true)
  price: number;

  // OR

  @Range(10, 100)
  @ExclusiveRange(true)
  price2: number;
}
```

```typescript [Functional API]
import {s} from "@tsed/schema";

const ProductSchema = s.object({
  price: s.number().customKey("range", [10, 100]).customKey("exclusiveRange", true)
});
```

:::

Finally, we can create a unit test to verify if our example works properly:

```typescript
import "@tsed/ajv";
import {PlatformTest} from "@tsed/platform-http/testing";
import {getJsonSchema} from "@tsed/schema";
import {Product} from "./Product";
import "../keywords/RangeKeyword";

describe("Product", () => {
  beforeEach(PlatformTest.create);
  afterEach(PlatformTest.reset);

  it("should call custom keyword validation (compile)", () => {
    const ajv = PlatformTest.get<Ajv>(Ajv);
    const schema = getJsonSchema(Product, {customKeys: true});
    const validate = ajv.compile(schema);

    expect(schema).to.deep.equal({
      properties: {
        price: {
          exclusiveRange: true,
          range: [10, 100],
          type: "number"
        }
      },
      type: "object"
    });

    expect(validate({price: 10.01})).toEqual(true);
    expect(validate({price: 99.99})).toEqual(true);
    expect(validate({price: 10})).toEqual(false);
    expect(validate({price: 100})).toEqual(false);
  });
});
```

## ~~Ignore~~

<Badge text="deprecated" type="warn"/>

The [Ignore](/ai/api/specs/schema/types/decorators/common/decorator-ignore.md) decorator is used to ignore a property in the JsonSchema generation and when you use the json-mapper.

But this decorator is deprecated and will be removed in the next major version of Ts.ED.
Instead, use Groups decorator to manage your model serialization/deserialization.

::: tip Note

To retrieve the original Ignore decorator behavior, you can use the [Groups](/ai/api/specs/schema/types/decorators/common/decorator-groups.md) decorator. You have to set
`jsonMapper.strictGroups` to `true` also:

```ts
@Configuration({
  jsonMapper: {
    strictGroups: true
  }
})
```

## Groups

[Groups](/ai/api/specs/schema/types/decorators/common/decorator-groups.md) decorator allows you to manage your serialized/deserialized fields by using group label. For example, with a
CRUD controller, you can have many methods like `POST`, `PUT`, `GET` or `PATCH` to manage `creation`, `update`
and `read` use cases for the exposed resource.

For the creation, you don't need to have the `id` field but for the update, you need to have it. With the previous
version for Ts.ED, you had to create the model twice, one for the `creation` (without `id`) and another one for `update`
and `read` (with `id`). Managing many models can be a pain point for the developer, this is why the [Groups](/ai/api/specs/schema/types/decorators/common/decorator-groups.md) decorator
exists.

For example, we have a User model with the following properties:

```ts
import {CollectionOf, Groups, Required} from "@tsed/schema";

export class User {
  @Groups("!creation")
  id: string;

  @Required()
  firstName: string;

  @Required()
  lastName: string;

  @Required()
  @Groups("group.email", "creation")
  email: string;

  @Groups("creation")
  password: string;

  @CollectionOf(String)
  @Groups("group.roles")
  roles: string[];
}
```

**Explanation:**

-   `!creation`: This annotation indicates that the field will never be exposed when using the `creation` group.
-   `group.email`: This annotation indicates that the field will be exposed only if the group match with `group.email` or
    with a glob pattern like `group.*`.

So by using the [deserialize](/ai/api/specs/json-mapper/types/utils/function-deserialize.md) function with the extra groups options, we can map data to the expected user instance:

::: code-group

```typescript [Creation]
import {deserialize} from "@tsed/json-mapper";

const result = deserialize<User>(
  {
    id: "id", // will be ignored because creation doesn't include `id` field
    firstName: "firstName",
    lastName: "lastName",
    email: "email@tsed.dev",
    password: "password"
  },
  {type: User, groups: ["creation"]}
);

console.log(result); // User {firstName, lastName, email, password}
```

```typescript [With group]
import {deserialize} from "@tsed/json-mapper";

const result = deserialize<User>(
  {
    id: "id",
    firstName: "firstName",
    lastName: "lastName",
    email: "email@tsed.dev",
    password: "password",
    roles: ["admin"]
  },
  {type: User, groups: ["group.email"]}
);

console.log(result); // User {id, firstName, lastName, email, password}
```

```typescript [With glob pattern]
import {deserialize} from "@tsed/json-mapper";

const result = deserialize<User>(
  {
    id: "id",
    firstName: "firstName",
    lastName: "lastName",
    email: "email@tsed.dev",
    password: "password",
    roles: ["admin"]
  },
  {type: User, groups: ["group.*"]}
);

console.log(result); // User {id, firstName, lastName, email, password, roles}
```

:::

::: tip Note

The same principle works with the [serialize](/ai/api/specs/json-mapper/types/utils/function-serialize.md) and [getJsonSchema](/ai/api/specs/schema/types/utils/function-get-json-schema.md) functions!

:::

Now let's see how groups work with controllers.

::: code-group

```ts [UsersCtrl.ts]
import {BodyParams, PathParams} from "@tsed/platform-params";
import {Get, Groups, Post, Returns} from "@tsed/schema";
import {Controller} from "@tsed/di";
import {User} from "../models/User";

@Controller("/")
export class UsersCtrl {
  @Get("/:id")
  @Returns(200, User).Groups("group.*")
  async get(@PathParams("id") id: string) {}

  @Post("/")
  @Returns(201, User).Groups("group.*")
  post(@BodyParams() @Groups("creation") user: User) {
    console.log(user); // User {firstName, lastName, email, password}
    user.id = uuid();

    return user; // will return Object {id, firstName, lastName, email}
  }
}
```

```ts [User.ts]
import {CollectionOf, Groups, Required} from "@tsed/schema";

export class User {
  @Groups("!creation")
  id: string;

  @Required()
  firstName: string;

  @Required()
  lastName: string;

  @Required()
  @Groups("group.email", "creation")
  email: string;

  @Groups("creation")
  password: string;

  @CollectionOf(String)
  @Groups("group.roles")
  roles: string[];
}
```

```json [OpenSpec]
{
  "paths": {
    "/{id}": {
      "get": {
        "operationId": "usersCtrlGet",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserGroup"
                }
              }
            },
            "description": "Success"
          }
        },
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "tags": ["UsersCtrl"]
      }
    },
    "/": {
      "post": {
        "operationId": "usersCtrlPost",
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserUser"
                }
              }
            },
            "description": "Created"
          }
        },
        "parameters": [],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UserCreation"
              }
            }
          }
        },
        "tags": ["UsersCtrl"]
      }
    }
  },
  "tags": [
    {
      "name": "UsersCtrl"
    }
  ],
  "components": {
    "schemas": {
      "UserGroup": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "firstName": {
            "type": "string",
            "minLength": 1
          },
          "lastName": {
            "type": "string",
            "minLength": 1
          },
          "email": {
            "type": "string",
            "minLength": 1
          },
          "roles": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": ["firstName", "lastName", "email"]
      },
      "UserCreation": {
        "type": "object",
        "properties": {
          "firstName": {
            "type": "string",
            "minLength": 1
          },
          "lastName": {
            "type": "string",
            "minLength": 1
          },
          "email": {
            "type": "string",
            "minLength": 1
          },
          "password": {
            "type": "string"
          }
        },
        "required": ["firstName", "lastName", "email"]
      }
    }
  }
}
```

:::

We can see that the [Groups](/ai/api/specs/schema/types/decorators/common/decorator-groups.md) decorator can be used on parameter level as well as on the method through the [Returns](/ai/api/specs/schema/types/decorators/operations/decorator-returns.md)
decorator. The generated OpenSpec will create automatically the appropriate JsonSchema according to the `groups`
configuration!

::: tip
You can combine different group labels or use a glob pattern to match multiple group labels. It's also possible
to use negation by prefixing the group label with `!`.
:::

## Groups strict mode <Badge text="7.69.0+"/>

The Groups decorator has introduced a big change in the way it manages its models,
but it can sometimes be complicated to understand its default behavior when the endpoint does not define Groups.

In addition, the documentation generated does not reflect the behavior observed in runtime, which adds confusion.

For example, we have our User model with the following properties and Groups configuration:

```ts
export class User {
  @Groups("!creation")
  id: string;

  @Required()
  firstName: string;

  @Required()
  lastName: string;

  @Required()
  @Groups("group.email", "creation")
  email: string;

  @Groups("creation")
  password: string;
}
```

Now, we have this controller:

```ts
class TestController {
  @Post("/")
  @Returns(200, User)
  async post(@BodyParams() user: User) {
    return user;
  }
}
```

We can see that the [Returns](/ai/api/specs/schema/types/decorators/operations/decorator-returns.md) and the input params doesn't set any group configuration. In this case, Ts.ED will not
apply any group configuration to the input params and the output.

So if you send this payload:

```json
{
  "id": "id",
  "firstName": "firstName",
  "lastName": "lastName",
  "email": "",
  "password": "password"
}
```

The endpoint will return the same payload without any modification. But here, we expect that the `email` and `password`
fields are not returned because they are Groups configuration on these `fields`.

To avoid this behavior, you can set the `strictGroups` option to the `json-mapper`:

```ts
@Configuration({
  jsonMapper: {
    strictGroups: true
  }
})
```

Now, if you send the same payload, the endpoint will return the following payload:

```json
{
  "id": "id",
  "firstName": "firstName",
  "lastName": "lastName"
}
```

::: warning
The `strictGroups` option is enabled by default in the next major version of Ts.ED.
:::

## Groups Name

By default, Groups decorator generate automatically a name for each model impacted by the given groups list.
If you use a typed client http generator based on Swagger (OAS3) to generate the client code, this behavior can be a
constraint for your consumer when you change the group list.

```ts
import {BodyParams, PathParams} from "@tsed/platform-params";
import {Get, Groups, Post, Returns} from "@tsed/schema";
import {Controller} from "@tsed/di";
import {User} from "../models/User";

@Controller("/")
export class UsersCtrl {
  @Get("/:id")
  @(Returns(200, User).Groups("group.*"))
  async get(@PathParams("id") id: string) {}

  @Post("/")
  @(Returns(201, User).Groups("group.*"))
  async post(@BodyParams() @Groups("creation", "summary") user: User) {}
}
```

In this example, the Groups annotation `@Groups("creation", "summary") user: User` will generate a new model name
`UserCreationSummary`.
If you change the `groups` list by this one:

```ts
export class UsersCtrl {
  async post(@BodyParams() @Groups("creation", "summary", "extra") user: User) {}
}
```

The new model name will be `UserCreationSummaryExtra`. This change will break the entire consumer code by removing the
`UserCreationSummary` type and giving a new `UserCreationSummaryExtra` type.
In fact, `UserCreationSummary` and `UserCreationSummaryExtra` are the same model with more fields!

To minimize the impact of this kind of change, Ts.ED lets you configure the postfix added to each model
impacted by groups.

Here is an example with a configured GroupsName:

```ts
import {BodyParams, PathParams} from "@tsed/platform-params";
import {Get, Groups, Post, Returns} from "@tsed/schema";
import {Controller} from "@tsed/di";
import {User} from "../models/User";

@Controller("/")
export class UsersCtrl {
  @Get("/:id")
  @(Returns(200, User).Groups("Details", ["group.*"]))
  async get(@PathParams("id") id: string) {}

  @Post("/")
  @(Returns(201, User).Groups("Details", ["group.*"]))
  async post(@BodyParams() @Groups("Creation", ["creation", "summary"]) user: User) {}
}
```

Now, `@Groups("Creation", ["creation", "summary"]) user: User` will generate a `UserCreation` type and
`@Returns(200, User).Groups("Details", ["group.*"])` will generate a `UserDetails` type.

## Groups class definition

It's also possible to define all groups on class instead of declaring it on each property.

::: code-group

```ts [UsersCtrl.ts]
import {BodyParams, PathParams} from "@tsed/platform-params";
import {Get, Groups, Post, Returns} from "@tsed/schema";
import {Controller} from "@tsed/di";
import {User} from "../models/User";

@Controller("/")
export class UsersCtrl {
  @Get("/:id")
  @Returns(200, User).Groups("update")
  async get(@PathParams("id") id: string) {}

  @Post("/")
  @Returns(201, User).Groups("update")
  post(@BodyParams() @Groups("creation") user: User) {
    console.log(user);
    user.id = uuid();

    return user;
  }

  @Post("/change-password")
  @Returns(204)
  changePassword(@BodyParams() @Groups("changePassword") user: User) {
    console.log(user);
  }
}
```

```ts [User.ts]
import {Groups, Required, RequiredGroups} from "@tsed/schema";

@Groups<User>({
  // will generate UserCreate
  create: ["firstName", "lastName", "email", "password"],
  // will generate UserUpdate
  update: ["id", "firstName", "lastName", "email"],
  // will generate UserChangePassword
  changePassword: ["id", "password", "newPassword"]
})
export class User {
  @Required()
  id: string;

  @RequiredGroups("creation")
  firstName: string;

  @RequiredGroups("creation")
  lastName: string;

  @RequiredGroups("creation")
  email: string;

  @RequiredGroups("create", "changePassword")
  password: string;

  @RequiredGroups("changePassword")
  newPassword: string;
}
```

```json [Creation]
{
  "properties": {
    "email": {
      "minLength": 1,
      "type": "string"
    },
    "firstName": {
      "minLength": 1,
      "type": "string"
    },
    "lastName": {
      "minLength": 1,
      "type": "string"
    },
    "password": {
      "type": "string"
    }
  },
  "required": ["firstName", "lastName", "email"],
  "type": "object"
}
```

```json [Update]
{
  "properties": {
    "email": {
      "type": "string"
    },
    "firstName": {
      "type": "string"
    },
    "id": {
      "minLength": 1,
      "type": "string"
    },
    "lastName": {
      "type": "string"
    }
  },
  "required": ["id"],
  "type": "object"
}
```

```json [ChangePassword]
{
  "properties": {
    "id": {
      "minLength": 1,
      "type": "string"
    },
    "newPassword": {
      "minLength": 1,
      "type": "string"
    },
    "password": {
      "minLength": 1,
      "type": "string"
    }
  },
  "required": ["id", "password", "newPassword"],
  "type": "object"
}
```

:::

## ForwardGroups

Groups configuration isn't forwarded to the nested models to avoid side effects on model generation.
With [ForwardGroups](/ai/api/specs/schema/types/decorators/common/decorator-forward-groups.md) decorator, you can specify whether a property should use the Groups configuration to
correctly generate a nested model.

```typescript
class ChildModel {
  @Groups("!creation")
  id: string;

  @Required()
  prop1: string;
}

class MyModel {
  @Groups("!creation")
  id: string;

  @Groups("group.summary")
  @Required()
  prop1: string;

  @Groups("group.extended")
  @Required()
  prop2: string;

  @Property()
  @Required()
  prop3: string;

  @CollectionOf(ChildModel)
  @ForwardGroups()
  prop4: ChildModel[];
}
```

Now `prop4` will have a `ChildModel` generated along to groups configuration.

## RequiredGroups

As [Groups](/ai/api/specs/schema/types/decorators/common/decorator-groups.md) decorator, [RequiredGroups](/ai/api/specs/schema/types/decorators/common/decorator-required-groups.md) allow you to define when a field is `required` depending on the given groups
strategy.

The usage is the same as Groups:

```typescript
import {RequiredGroups, Groups, Required} from "@tsed/schema";

class MyModel {
  @Groups("!creation")
  id: string;

  @Required()
  prop1: string;

  @RequiredGroups("!patch")
  @Required()
  prop2: string;

  @RequiredGroups("patch")
  @Required()
  prop3: string;
}
```

## AllowedGroups

This feature lets your API consumers define which fields they want to consume. The server will automatically filter
fields based on the [Groups](/ai/api/specs/schema/types/decorators/common/decorator-groups.md) strategy.

```typescript
class MyModel {
  @Property()
  id: string;

  @Property()
  description: string;

  @Groups("summary")
  prop1: string; // not display by default

  @Groups("details")
  prop2: string; // not display by default

  @Groups("admin")
  sensitiveProp: string; // not displayed because it's a sensitive props
}

@Controller("/controllers")
class MyController {
  @Get("/:id")
  @(Returns(200, MyModel).Groups("!admin").AllowedGroups("summary", "details"))
  get() {
    return {
      id: "id",
      description: "description",
      prop1: "prop1",
      prop2: "prop2",
      sensitiveProp: "sensitiveProp"
    };
  }
}
```

AllowedGroups is enabled when the `includes` query parameter is provided in the request. Here are the different
scenarios with
this parameter:

::: code-group

```text [Basic usage]
# Request
GET http://host/rest/controllers/1?includes=summary

# Response
{
  "id": "id",
  "description": "description",
  "prop1": "prop1"
}
```

```text [Multiple includes]
# Request
GET http://host/rest/controllers/1?includes=summary&includes=details

OR

GET http://host/rest/controllers/1?includes=summary,details

# Response
{
  "id": "id",
  "description": "description",
  "prop1": "prop1",
  "prop2": "prop2"
}
```

```text [Without includes]
# Request
GET http://host/rest/controllers/1

# Response
{
  "id": "id",
  "description": "description",
  "prop1": "prop1",
  "prop2": "prop2"
}
```

```text [Unexpected includes]
# Request
GET http://host/rest/controllers/1?includes=admin

# Response
{
  "id": "id",
  "description": "description",
  "prop1": "prop1",
  "prop2": "prop2"
}
```

:::

## Partial

Partial allows you to create a Partial model on an endpoint:

```typescript
import {Returns, Patch, Partial} from "@tsed/schema";
import {Controller} from "@tsed/di";
import {BodyParams} from "./bodyParams";

@Controller("/")
class MyController {
  @Patch("/")
  @(Returns(200, MyModel).Groups("group.*"))
  async patch(@BodyParams() @Partial() payload: MyModel) {
    // ...
  }
}
```

## Advanced validation

### BeforeDeserialize

If you want to validate or manipulate data before the model has been deserialized you can use the [BeforeDeserialize](/ai/api/specs/json-mapper/types/decorators/decorator-before-deserialize.md)
decorator.

::: tip Note

Remember to return the data in your callback function, otherwise an error will occur.

:::

```typescript
import {Enum, Property} from "@tsed/schema";
import {BeforeDeserialize} from "@tsed/json-mapper";
import {BadRequest} from "@tsed/exceptions";

enum AnimalType {
  DOG = "DOG",
  CAT = "CAT"
}

@BeforeDeserialize((data: Record<string, unknown>) => {
  if (data.type !== AnimalType.DOG) {
    throw new BadRequest("Sorry, we're only responsible for dogs");
  } else {
    data.name = `Our dog ${data.name}`;
    return data;
  }
})
export class Animal {
  @Property()
  name: string;

  @Enum(AnimalType)
  type: AnimalType;
}
```

### AfterDeserialize

If you want to validate or manipulate data after the model has been deserialized you can use the [AfterDeserialize](/ai/api/specs/json-mapper/types/decorators/decorator-after-deserialize.md)
decorator.

::: tip Note

Remember to return the data in your callback function, otherwise an error will occur.

:::

```typescript
import {Enum, Property} from "@tsed/schema";
import {AfterDeserialize} from "@tsed/json-mapper";
import {BadRequest} from "@tsed/exceptions";

enum AnimalType {
  DOG = "DOG",
  CAT = "CAT"
}

@AfterDeserialize((data: Animal) => {
  if (data.type !== AnimalType.CAT) {
    throw new BadRequest("Sorry, we're only responsible for cats");
  } else {
    data.name = `Our cat ${data.name}`;
    return data;
  }
})
export class Animal {
  @Property()
  name: string;
  @Enum(AnimalType)
  type: AnimalType;
}
```

### Custom validation decorator

Validation can quickly become complex and therefore confusing. In this case you can use your own validation decorator.

```typescript
import {BeforeDeserialize} from "@tsed/json-mapper";
import {Property, JsonEntityFn} from "@tsed/schema";
import {BadRequest} from "@tsed/exceptions";

class Company {
  @Property()
  name: string;

  @Property()
  @RequiredIf((value: any, data: any) => data.name === "tsed" && value !== undefined)
  location: string;
}

function RequiredIf(cb: any): PropertyDecorator {
  return JsonEntityFn((store, [target, propertyKey]) => {
    BeforeDeserialize((data) => {
      if (!cb(data[propertyKey], data)) {
        throw new BadRequest(`${String(propertyKey)} is required`);
      }
      return data;
    })(target);
  });
}
```

## Discriminator <Badge text="v7.8.0+" />

The discriminator feature allows polymorphism with JsonSchema and OpenAPI.
Although [OneOf](/ai/api/specs/schema/types/decorators/common/decorator-one-of.md) already allows polymorphism in terms of validation, the latter doesn't allow the `@tsed/json-mapper`
to render the correct class type during the deserialization (plain object to class).

By declaring a discriminatorKey, `@tsed/json-mapper` will be able to determine the correct class which should be used.

Here is an example:

```typescript
import {DiscriminatorKey, DiscriminatorValue, OneOf, Property, Required} from "@tsed/schema";

export enum EventType {
  PAGE_VIEW = "page_view",
  ACTION = "action",
  CLICK_ACTION = "click_action"
}

export class Event {
  @DiscriminatorKey() // declare this property as discriminator key
  type: string; // Note: Do not set EventType enum here. The @DiscriminatorKey decorator will automatically generate the correct values based on @DiscriminatorValue decorators in derived classes.

  @Property()
  value: string;
}

@DiscriminatorValue(EventType.PAGE_VIEW)
// or @DiscriminatorValue() value can be inferred by the class name (ex: "page_view")
export class PageView extends Event {
  override type = EventType.PAGE_VIEW; // optional

  @Required()
  url: string;
}

@DiscriminatorValue(EventType.ACTION, EventType.CLICK_ACTION)
export class Action extends Event {
  @Required()
  event: string;
}

export class Tracking {
  @OneOf(Action, PageView)
  data: Action | PageView;
}
```

And now we can use `deserialize` to map a plain object to a class:

```typescript
import {deserialize} from "@tsed/json-mapper";
import {Tracking} from "./Tracking";

const list = {
  data: [
    {
      type: "page_view",
      value: "value",
      url: "https://url"
    },
    {
      type: "action",
      value: "value",
      event: "event"
    },
    {
      type: "click_action",
      value: "value",
      event: "event"
    }
  ]
};

const result = deserialize(list, {
  type: Tracking
});

expect(result.data[0]).toBeInstanceOf(PageView);
expect(result.data[1]).toBeInstanceOf(Action);
expect(result.data[2]).toBeInstanceOf(Action);
expect(result.data[3]).toBeInstanceOf(CustomAction);
```

::: tip Shortcut

Declaring each time, the list of children class using [OneOf](/ai/api/specs/schema/types/decorators/common/decorator-one-of.md) decorator can be a pain point, so Ts.ED provide a way to
simplify your code:

Instead of declaring all classes:

```ts
export class Tracking {
  @OneOf(Action, PageView)
  data: Action | PageView;
}
```

Give the parent class to `OneOf` decorator:

```typescript
export type EventsType = Action | PageView;

export class Tracking {
  @OneOf(Event)
  data: EventsType;
}
```

Ts.ED will automatically infer the children's classes!
:::

Discriminator model can be used also on controller:

```typescript
@Controller("/")
class Test {
  @Put("/:id")
  @(Returns(200).OneOf(Event))
  put(@PathParams(":id") id: string, @BodyParams() @OneOf(Event) event: EventsType) {
    return [];
  }
}
```

## Generics

### Declaring a generic model

Sometimes, it might be useful to use generic models. TypeScript doesn't store the generic type in the metadata. This is
why we need to declare explicitly the generic models with the decorators.

One of the generic's usage can be a paginated list. With Returns decorator, it's now possible to declare a generic type
and generate the appropriate OpenSpec documentation.

Starting with the pagination model, by using [Generics](/ai/api/specs/schema/types/decorators/generics/decorator-generics.md) and [CollectionOf](/ai/api/specs/schema/types/decorators/collections/decorator-collection-of.md):

```ts
import {CollectionOf, Generics, Property} from "@tsed/schema";

@Generics("T")
class Pagination<T> {
  @CollectionOf("T")
  data: T[];

  @Property()
  totalCount: number;
}
```

Now, we need a model to be used with the generic Pagination model:

```ts
import {Property} from "@tsed/schema";

class Product {
  @Property()
  id: string;

  @Property()
  title: string;
}
```

Finally, we can use our models on a method as follows:

::: code-group

```ts [MyController.ts]
import {Post, Returns} from "@tsed/schema";
import {Controller} from "@tsed/di";
import {Pagination} from "../models/Pagination";
import {Product} from "../models/Product";

@Controller("/")
class MyController {
  @Post("/")
  @Returns(200, Pagination).Of(Product).Description("description")
  method(): Promise<Pagination<Product> | null> {
    return Promise.resolve(null);
  }
}
```

```json [Swagger 2]
{
  "definitions": {
    "Product": {
      "properties": {
        "title": {
          "type": "string"
        }
      },
      "type": "object"
    },
    "Submission": {
      "properties": {
        "_id": {
          "type": "string"
        },
        "data": {
          "$ref": "#/definitions/Product"
        }
      },
      "type": "object"
    }
  },
  "tags": [
    {
      "name": "MyController"
    }
  ],
  "paths": {
    "/": {
      "post": {
        "operationId": "myControllerMethod",
        "tags": ["MyController"],
        "parameters": [
          {
            "in": "body",
            "name": "body",
            "required": false,
            "schema": {
              "$ref": "#/definitions/Submission"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success"
          }
        }
      }
    }
  }
}
```

```json [OpenAPI 3]
{
  "components": {
    "schemas": {
      "Product": {
        "properties": {
          "title": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "Submission": {
        "properties": {
          "_id": {
            "type": "string"
          },
          "data": {
            "$ref": "#/components/schemas/Product"
          }
        },
        "type": "object"
      }
    }
  },
  "paths": {
    "/": {
      "post": {
        "operationId": "myControllerMethod",
        "parameters": [],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Submission"
              }
            }
          },
          "required": false
        },
        "responses": {
          "200": {
            "description": "Success"
          }
        },
        "tags": ["MyController"]
      }
    }
  },
  "tags": [
    {
      "name": "MyController"
    }
  ]
}
```

:::

### Declaring nested generic models

It's also possible to declare nested generic models to have this type `Pagination<Submission<Product>>`:

::: code-group

```typescript [MyController.ts]
import {Post, Generics, Property, Returns} from "@tsed/schema";

class MyController {
  @Post("/")
  @(Returns(200, Pagination).Of(Submission).Nested(Product).Description("description"))
  async method(): Promise<Pagination<Submission<Product>> | null> {
    return null;
  }
}
```

```ts [Submission.ts]
import {Generics, Property} from "@tsed/schema";

@Generics("T")
class Submission<T> {
  @Property()
  _id: string;

  @Property("T")
  data: T;
}
```

```ts [Pagination.ts]
import {CollectionOf, Generics, Property} from "@tsed/schema";

@Generics("T")
class Pagination<T> {
  @CollectionOf("T")
  data: T[];

  @Property()
  totalCount: number;
}
```

```ts [Product.ts]
import {Property} from "@tsed/schema";

class Product {
  @Property()
  id: string;

  @Property()
  title: string;
}
```

```json [OpenSpec 2]
{
  "definitions": {
    "Product": {
      "properties": {
        "title": {
          "type": "string"
        }
      },
      "type": "object"
    }
  },
  "paths": {
    "/": {
      "post": {
        "operationId": "myControllerMethod",
        "parameters": [],
        "produces": ["application/json"],
        "responses": {
          "200": {
            "description": "description",
            "schema": {
              "properties": {
                "data": {
                  "items": {
                    "properties": {
                      "_id": {
                        "type": "string"
                      },
                      "data": {
                        "$ref": "#/definitions/Product"
                      }
                    },
                    "type": "object"
                  },
                  "type": "array"
                },
                "totalCount": {
                  "type": "number"
                }
              },
              "type": "object"
            }
          }
        },
        "tags": ["MyController"]
      }
    }
  },
  "tags": [
    {
      "name": "MyController"
    }
  ]
}
```

```json [OpenSpec 3]
{
  "components": {
    "schemas": {
      "Product": {
        "properties": {
          "title": {
            "type": "string"
          }
        },
        "type": "object"
      }
    }
  },
  "paths": {
    "/": {
      "post": {
        "operationId": "myControllerMethod",
        "parameters": [],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "data": {
                      "items": {
                        "properties": {
                          "_id": {
                            "type": "string"
                          },
                          "data": {
                            "$ref": "#/components/schemas/Product"
                          }
                        },
                        "type": "object"
                      },
                      "type": "array"
                    },
                    "totalCount": {
                      "type": "number"
                    }
                  },
                  "type": "object"
                }
              }
            },
            "description": "description"
          }
        },
        "tags": ["MyController"]
      }
    }
  },
  "tags": [
    {
      "name": "MyController"
    }
  ]
}
```

:::

### Generics with Types

::: code-group

```ts [String]
import {GenericOf, Generics, getJsonSchema, Property} from "@tsed/schema";

@Generics("T")
class UserProperty<T> {
  @Property("T")
  value: T;
}

class Adjustment {
  @GenericOf(String)
  adjustment: UserProperty<string>;
}

console.log(getJsonSchema(Adjustment));
/* OUTPUT:
{
  "properties": {
    "adjustment": {
      "properties": {
        "value": {
          "type": "string"
        }
      },
      "type": "object"
    }
  },
  "type": "object"
}
*/
```

```ts [Date]
import {GenericOf, Generics, getJsonSchema, Property} from "@tsed/schema";

@Generics("T")
class UserProperty<T> {
  @Property("T")
  value: T;
}

class Adjustment {
  @GenericOf(Date)
  adjustment: UserProperty<Date>;
}

console.log(getJsonSchema(Adjustment));
/* OUTPUT:
{
  "properties": {
    "adjustment": {
      "properties": {
        "value": {
          "format": "date-time",
          "type": "string"
        }
      },
      "type": "object"
    }
  },
  "type": "object"
}
*/
```

```ts [Enum]
import {GenericOf, Generics, getJsonSchema, Property} from "@tsed/schema";

enum AdjustmentType {
  PRICE = "price",
  DELAY = "delay"
}

@Generics("T")
class UserProperty<T> {
  @Property("T")
  value: T;
}

class Adjustment {
  @GenericOf(AdjustmentType)
  adjustment: UserProperty<AdjustmentType>;
}

console.log(getJsonSchema(Adjustment));
/* OUTPUT:
{
  "properties": {
    "adjustment": {
      "properties": {
        "value": {
          "enum": [
            "price"
            "delay"
          ],
          "type": "string"
        }
      },
      "type": "object"
    }
  },
  "type": "object"
}
*/
```

:::

### Generics with Functional API

::: code-group

```ts [String]
import {GenericOf, Generics, getJsonSchema, Property, string} from "@tsed/schema";

@Generics("T")
class UserProperty<T> {
  @Property("T")
  value: T;
}

class Adjustment {
  @GenericOf(string().pattern(/[a-z]/))
  adjustment: UserProperty<string>;
}

console.log(getJsonSchema(Adjustment));
/* OUTPUT:
{
  "properties": {
    "adjustment": {
      "properties": {
        "value": {
          "type": "string",
          "pattern": "[a-z]"
        }
      },
      "type": "object"
    }
  },
  "type": "object"
}
*/
```

```ts [Date]
import {date, GenericOf, Generics, getJsonSchema, Property, string} from "@tsed/schema";

@Generics("T")
class UserProperty<T> {
  @Property("T")
  value: T;
}

class Adjustment {
  @GenericOf(date().format("date-time"))
  adjustment: UserProperty<Date>;
}

console.log(getJsonSchema(Adjustment));
/* OUTPUT:
{
  "properties": {
    "adjustment": {
      "properties": {
        "value": {
          "type": "string",
          "format": "date-time"
        }
      },
      "type": "object"
    }
  },
  "type": "object"
}
*/
```

:::

## Pagination

The following advanced example will show you how you can combine the different Ts.ED features to describe Pagination.
The used features are the following:

-   [Generics](/docs/model.html#generics)
-   [Function programming to declare models](/docs/model.html#using-functions)
-   [For](/ai/api/specs/schema/types/decorators/common/decorator-for.md) decorator to declare a custom model for JsonSchema, OS2 or OS3.
-   [Response Filter](/docs/response-filter.md) to manage paginated response.

::: code-group

```ts [ProductsCtrl.ts]
import {QueryParams} from "@tsed/platform-params";
import {Get, Returns} from "@tsed/schema";
import {Controller} from "@tsed/di";
import {Pageable} from "../models/Pageable";
import {Pagination} from "../models/Pagination";
import {Product} from "../models/Product";

@Controller("/pageable")
class ProductsCtrl {
  @Get("/")
  @Returns(206, Pagination).Of(Product).Title("PaginatedProduct")
  @Returns(200, Pagination).Of(Product).Title("PaginatedProduct")
  get(@QueryParams() pageableOptions: Pageable, @QueryParams("all") all: boolean) {
    return new Pagination<Product>({
      data: [
        new Product({
          id: "100",
          title: "CANON D3000"
        })
      ],
      totalCount: all ? 1 : 100, // just for test,
      pageable: pageableOptions
    });
  }
}
```

```ts [Pageable.ts]
import {isString} from "@tsed/core";
import {OnDeserialize} from "@tsed/json-mapper";
import {array, Default, Description, For, Integer, Min, oneOf, SpecTypes, string} from "@tsed/schema";

class Pageable {
  @Integer()
  @Min(0)
  @Default(0)
  @Description("Page number.")
  page: number = 0;

  @Integer()
  @Min(1)
  @Default(20)
  @Description("Number of objects per page.")
  size: number = 20;

  @For(SpecTypes.JSON, oneOf(string(), array().items(string()).maxItems(2)))
  @For(SpecTypes.OPENAPI, array().items(string()).maxItems(2))
  @OnDeserialize((value: string | string[]) => (isString(value) ? value.split(",") : value))
  @Description(
    "Sorting criteria: property(,asc|desc). Default sort order is ascending. Multiple sort criteria are supported."
  )
  sort: string | string[];

  constructor(options: Partial<Pageable>) {
    options.page && (this.page = options.page);
    options.size && (this.size = options.size);
    options.sort && (this.sort = options.sort);
  }

  get offset() {
    return this.page ? this.page * this.limit : 0;
  }

  get limit() {
    return this.size;
  }
}
```

```ts [Pagination.ts]
import {CollectionOf, Default, Generics, Integer, MinLength} from "@tsed/schema";
import {Pageable} from "./Pageable";

export class PaginationLink {
  @Property()
  next: string;

  @Property()
  prev: string;
}

@Generics("T")
export class Pagination<T> extends Pageable {
  @CollectionOf("T")
  data: T[];

  @Integer()
  @MinLength(0)
  @Default(0)
  totalCount: number = 0;

  @Property()
  links: PaginationLink = new PaginationLink();

  constructor({data, totalCount, pageable}: Partial<Pagination<T>> & {pageable: Pageable}) {
    super(pageable);
    data && (this.data = data);
    totalCount && (this.totalCount = totalCount);
  }
}
```

```ts [Product.ts]
import {Property} from "@tsed/schema";

class Product {
  @Property()
  id: string;

  @Property()
  title: string;

  constructor({id, title}: Partial<Product> = {}) {
    id && (this.id = id);
    title && (this.title = title);
  }
}
```

```ts [PaginationFilter.ts]
import {PlatformContext} from "@tsed/platform-http";
import {ResponseFilter, ResponseFilterMethods} from "@tsed/platform-response-filter";
import {Pagination} from "../models/Pagination";

@ResponseFilter("application/json")
class PaginationFilter implements ResponseFilterMethods {
  transform(data: unknown, ctx: PlatformContext): any {
    if (ctx.data instanceof Pagination) {
      // /!\ don't modify the ctx.data. at this step, the serializer has already been called.

      if (ctx.data.totalCount > (ctx.data.pageable.page + 1) * ctx.data.pageable.size) {
        ctx.response.status(206);
        data.links.next = `${ctx.request.url}?page=${ctx.data.pageable.page + 1}&size=${ctx.data.pageable.size}`;
      }

      if (ctx.data.pageable.page > 0) {
        data.links.prev = `${ctx.request.url}?page=${ctx.data.pageable.page - 1}&size=${ctx.data.pageable.size}`;
      }
    }

    return data;
  }
}
```

```ts [ProductsCtrl.spec.ts]
import {Property} from "@tsed/schema";

class Product {
  @Property()
  id: string;

  @Property()
  title: string;

  constructor({id, title}: Partial<Product> = {}) {
    id && (this.id = id);
    title && (this.title = title);
  }
}
```

:::

## Deep object on query

With OpenAPI 3, it's possible to describe and use
a [deepObject](https://swagger.io/docs/specification/serialization/#query) `style` as Query params.
It means, a consumer can call your endpoint with the following url:

```
/users?id[role]=admin&id[firstName]=Alex
```

Ts.ED will determine automatically the appropriate `style` parameter based on the given `User` model.
Here is an example with a DeepQueryObject model:

```typescript
class DeepQueryObject {
  @Property()
  path: string;

  @Property()
  condition: string;

  @Property()
  value: string;
}

@Path("/test")
class TestDeepObjectCtrl {
  @OperationPath("GET", "/")
  async get(@QueryParams("s") q: DeepQueryObject) {}
}
```

The url to be called will be:

```
/test?s[path]=title&s[condition]=eq&s[value]=tsed
```

And the generated swagger will be:

```json
{
  "components": {
    "schemas": {
      "DeepQueryObject": {
        "properties": {
          "condition": {
            "type": "string"
          },
          "path": {
            "type": "string"
          },
          "value": {
            "type": "string"
          }
        },
        "type": "object"
      }
    }
  },
  "paths": {
    "/test": {
      "get": {
        "operationId": "testDeepObjectCtrlGet",
        "parameters": [
          {
            "in": "query",
            "name": "s",
            "required": false,
            "style": "deepObject",
            "schema": {
              "$ref": "#/components/schemas/DeepQueryObject"
            }
          }
        ]
      }
    }
  }
}
```

::: tip

Ts.ED support also Generics Deep object style!

```typescript
class FindQuery {
  @Property()
  tableColumnNameA?: number;

  @Property()
  tableColumnNameB?: number;
}

@Generics("T")
class PaginationQuery<T> {
  @Minimum(0)
  @Default(0)
  offset?: number;

  @Minimum(1)
  @Maximum(1000)
  @Default(50)
  limit?: number;

  @Property("T")
  where?: T;
}

@Path("/test")
class TestDeepObjectCtrl {
  @OperationPath("GET", "/")
  async get(@QueryParams() @GenericOf(FindQuery) q: PaginationQuery<FindQuery>) {}
}
```

:::

::: warning
This feature is only available for OpenAPI 3.
:::

## Annotations

JSON Schema includes a few keywords and Ts.ED provide also theses corresponding decorators like [Title](/ai/api/specs/schema/types/decorators/common/decorator-title.md),
[Description](/ai/api/specs/schema/types/decorators/common/decorator-description.md), [Default](/ai/api/specs/schema/types/decorators/common/decorator-default.md), [Example](/ai/api/specs/schema/types/decorators/common/decorator-example.md) that aren’t strictly used for validation, but are used to describe parts of a
schema.

None of these `annotation` keywords are required, but they are encouraged for good practice, and can make your
schema `self-documenting`.

::: code-group

```ts [Model]
import {Default, Description, Example, Title} from "@tsed/schema";

export class Model {
  @Title("title")
  @Example("example")
  @Description("Description")
  @Default("default")
  prop: string = "default";
}
```

```json [JSON schema]
{
  "definitions": {},
  "properties": {
    "prop": {
      "default": "default",
      "description": "Description",
      "examples": ["example"],
      "title": "title",
      "type": "string"
    }
  },
  "type": "object"
}
```

:::

## Alias

[Name](/ai/api/specs/schema/types/decorators/common/decorator-name.md) decorator lets you rename the exposed property in your json schema.

For example mongo db uses the `_id` property. In order not to give any indication to our consumer about the nature of
the database, it's better to rename the property to `id`.

```ts
import {Description, Example, Name} from "@tsed/schema";
import {ObjectID} from "@tsed/mongoose";

export class Model {
  @Name("id")
  @Description("Object ID")
  @Example("5ce7ad3028890bd71749d477")
  _id: string;
}

// same example with mongoose
export class Model2 {
  @ObjectID("id")
  _id: string;
}
```

## Custom Schema

If Ts.ED doesn't provide the expected decorator to describe your json schema, you can use the [Schema](/ai/api/specs/schema/types/decorators/common/decorator-schema.md) decorator
from `@tsed/schema` to set a custom schema.

### Using JsonSchemaObject

You can declare schema by using the [JsonSchemaObject](/ai/api/specs/schema/types/domain/interface-json-schema-object.md) interface:

```ts
import {BodyParams} from "@tsed/platform-params";
import {JsonSchemaObject, Post, Returns, Schema} from "@tsed/schema";
import {Controller} from "@tsed/di";

const ProductSchema: JsonSchemaObject = {
  type: "object",
  properties: {}
};

export class MyModel {
  @Schema({
    contains: {
      type: "string"
    }
  })
  prop: string;
}

@Controller("/")
class MyController {
  @Post("/")
  @Returns(200).Description("description").Schema(ProductSchema)
  method(@BodyParams() @Schema(ProductSchema) product: any): Promise<null> {
    return Promise.resolve(null);
  }
}
```

### Using Functional API

It's also possible to write a valid JsonSchema by using the functional approach (Joi or Zod like):

```ts
import {BodyParams} from "@tsed/platform-params";
import {Post} from "@tsed/schema";
import {Controller} from "@tsed/di";
import {array, number, object, Returns, Schema, string} from "@tsed/schema";

const ProductSchema = object({
  id: string().required().description("Product ID"),
  title: string().required().minLength(3).example("CANON D300").description("Product title"),
  price: number().minimum(0).example(100).description("Product price"),
  description: string().description("Product description"),
  tags: array()
    .minItems(1)
    .items(string().minLength(2).maxLength(10).description("Product tag"))
    .description("Product tags")
}).label("ProductModel");

@Controller("/")
class MyController {
  @Post("/")
  @Returns(200).Description("description").Schema(ProductSchema)
  method(@BodyParams() @Schema(ProductSchema) product: any): Promise<null> {
    return Promise.resolve(null);
  }
}
```

Here is the list of available functions:

<ApiList query="status.includes('schemaFunctional')" />

#### Composing object schemas

JsonSchema instances returned by the Functional API expose `.pick()`, `.omit()`, `.partial()`, and `.merge()` helpers to
build derived object models without mutating the original schema. Each helper keeps `s.infer()` aligned with the
resulting shape.

```typescript [Functional API]
const User = s.object({
  id: s.string().required(),
  email: s.string(),
  password: s.string(),
  audit: s.object({
    createdAt: s.date().required(),
    updatedAt: s.date().optional()
  })
});

const PublicUser = User.pick("id", "email");
const InternalUser = User.omit("password");
const DraftUser = User.partial();
const ExtraFields = s.object({
  flags: s.object({
    active: s.boolean()
  })
});

const AuditedUser = User.merge(ExtraFields);

type PublicShape = s.infer<typeof PublicUser>;
// { id: string; email: string | undefined }

type InternalShape = s.infer<typeof InternalUser>;
// { id: string; email: string | undefined; audit: { createdAt: Date; updatedAt: Date | undefined } }

type DraftShape = s.infer<typeof DraftUser>;
// { id?: string | undefined; email?: string | undefined; password?: string | undefined; audit?: { createdAt: Date; updatedAt: Date | undefined } }

type AuditedShape = s.infer<typeof AuditedUser>;
// { id: string; email: string | undefined; password: string; audit: {...}; flags: { active: boolean } }
```

## Infer types (Experimental)

Ts.ED provides a Functional API to build JsonSchema programmatically with TypeScript type inference, similar to Zod.
You can derive the TypeScript type from a schema via `s.infer<typeof Schema>`.

```ts [Functional API]
import {s} from "@tsed/schema";

// Basic primitives
const Name = s.string();
const Age = s.number();

// Dates infer as Date by default (aligned with @tsed/json-mapper)
const CreatedAt = s.datetime();
const TimeOfDay = s.time();

// Collections
const Tags = s.set(s.string()); // Set<string>
const FlagsByKey = s.map(s.boolean()); // Record<string, boolean>

// Object with nested schemas
export const UserSchema = s.object({
  id: s.string().required(),
  email: s.string().optional(),
  profile: s.object({
    firstName: s.string(),
    lastName: s.string().optional()
  })
});

// any():
// - without args => any
// - with args => tuple of provided types
const AnyValue = s.any(); // any
const MixedTuple = s.any(s.string(), s.number(), s.boolean()); // [string, number, boolean]

// Infer a TS type from a schema
export type User = s.infer<typeof UserSchema>;
```

## RecordOf

The [RecordOf](/ai/api/specs/schema/types/decorators/common/decorator-record-of.md) decorator constructs a json schema object type which property keys are set by a given set of keys
and which property values are of a given type.

::: code-group

```ts [Decorators]
import {RecordOf} from "@tsed/schema";

type keys = "tech" | "hr";

class Department {
  employeeSize: number;
}

type Departments = Record<keys, Department>;

class Company {
  @RecordOf(Department, "tech", "hr")
  departments: Departments;
}
```

```json [JSON Schema]
{
  "definitions": {
    "Department": {
      "type": "object"
    }
  },
  "type": "object",
  "properties": {
    "Departments": {
      "properties": {
        "tech": {
          "$ref": "#/definitions/Department"
        },
        "hr": {
          "$ref": "#/definitions/Department"
        }
      },
      "type": "object"
    }
  }
}
```

:::

## Get Json schema

In some cases, it may be useful to retrieve the JSON Schema from a Model to use with another library. This is possible
by using [getJsonSchema](/ai/api/specs/schema/types/utils/function-get-json-schema.md). Here is a small example:

::: code-group

```ts [Model]
import {getJsonSchema, MinLength, Required} from "@tsed/schema";

class PersonModel {
  @MinLength(3)
  @Required()
  firstName: string;

  @MinLength(3)
  @Required()
  lastName: string;
}

const schema = getJsonSchema(PersonModel);

console.log(schema);
```

```json [JSON Schema]
{
  "definitions": {},
  "properties": {
    "firstName": {
      "minLength": 3,
      "type": "string"
    },
    "lastName": {
      "minLength": 3,
      "type": "string"
    }
  },
  "required": ["firstName", "lastName"],
  "type": "object"
}
```

:::

## Expose a JsonSchema

You can create a controller, or an endpoint to expose a specific schema with the custom keys. This can allow your
consumers to retrieve a validation template so that they can use it to validate a form.

```typescript
import {Controller} from "@tsed/di";
import {Get, getJsonSchema} from "@tsed/schema";
import {Product} from "../models/Product";

@Controller("/products")
export class ProductsCtrl {
  @Get("/.schema")
  get(@QueryParams("customKeys") customKeys: boolean, @QueryParams("groups") groups: string[]) {
    return getJsonSchema(Product, {customKeys, groups});
  }
}
```

## Get OpenSpec

In some cases, it may be useful to retrieve the OpenSpec from a Controller to generate the Swagger OpenSpec. This is
possible by using [getSpec](/ai/api/specs/schema/types/utils/function-get-spec.md). Here is a small example:

::: code-group

```ts [MyController]
import {getSpec, Post, Returns, SpecTypes} from "@tsed/schema";
import {Controller} from "@tsed/di";
import {Pagination} from "../models/Pagination";
import {Product} from "../models/Product";

@Controller("/")
class MyController {
  @Post("/")
  @Returns(200, Pagination).Of(Product).Description("description")
  method(): Promise<Pagination<Product> | null> {
    return Promise.resolve(null);
  }
}

const spec = getSpec(MyController, {specType: SpecTypes.OPENAPI});

console.log(spec);
```

```json [OpenSpec]
{
  "components": {
    "schemas": {
      "Product": {
        "properties": {
          "title": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "Submission": {
        "properties": {
          "_id": {
            "type": "string"
          },
          "data": {
            "$ref": "#/components/schemas/Product"
          }
        },
        "type": "object"
      }
    }
  },
  "paths": {
    "/": {
      "post": {
        "operationId": "myControllerMethod",
        "parameters": [],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Submission"
              }
            }
          },
          "required": false
        },
        "responses": {
          "200": {
            "description": "Success"
          }
        },
        "tags": ["MyController"]
      }
    }
  },
  "tags": [
    {
      "name": "MyController"
    }
  ]
}
```

:::

## Decorators

<ApiList query="status.includes('decorator') && status.includes('schema')" />
````
