800 lines
19 KiB
Markdown
800 lines
19 KiB
Markdown
# Structural
|
|
|
|
[](https://www.npmjs.com/package/structural)
|
|
[](https://codeclimate.com/github/reissbaker/structural/maintainability)
|
|
[](https://codeclimate.com/github/reissbaker/structural/test_coverage)
|
|
[](https://circleci.com/gh/reissbaker/structural)
|
|
|
|
Structural is a __runtime type checker__ for JavaScript and TypeScript that
|
|
allows you to execute type-checking code on data you only have access to at
|
|
runtime, like JSON data from network requests, YAML files from disk, or the
|
|
results of SQL queries. Structural is written in TypeScript and has deep
|
|
integration with its type system, allow TypeScript users to automatically get
|
|
compile-time type inference for their Structural types in addition to runtime
|
|
type checking. Structural types can also be automatically converted to actual,
|
|
executable TypeScript automatically, for generating documentation or
|
|
integrating with tools that understand TS type syntax, or converted to JSON
|
|
Schema for integrating with non-JS/TS-based tooling.
|
|
|
|
### Table of contents
|
|
|
|
* [Why?](#why)
|
|
* [TypeScript integration](#typescript-integration)
|
|
* [Comparisons](#comparisons)
|
|
* [Structural](#structural-1)
|
|
* [JSON Schema](#json-schema)
|
|
* [Advanced type system features](#advanced-type-system-features)
|
|
* [Custom validations](#custom-validations)
|
|
* [Slicing keys](#slicing-keys)
|
|
* [Generating TypeScript](#generating-typescript)
|
|
* [Generating JSON Schema](#generating-json-schema)
|
|
|
|
## Why?
|
|
|
|
Typically with data received at runtime, you're forced to do one of the
|
|
following:
|
|
|
|
1. Write a bunch of `if` statements to validate each piece of data;
|
|
2. Write piles of schema validation code in various verbose languages (e.g.
|
|
JSON Schema, XML DTDs / Relax-NG / Schema / etc.);
|
|
3. Or skip validating the data and pray.
|
|
|
|
Structural allows you to skip writing validation code and instead encode
|
|
validation logic into types defined in TypeScript or JavaScript; types are less
|
|
verbose to write and can live inside the same source files as the rest of your
|
|
code.
|
|
|
|
Here's a simple example:
|
|
|
|
```typescript
|
|
import { t } from "structural";
|
|
|
|
// Define a User type
|
|
const User = t.subtype({
|
|
id: t.num,
|
|
name: t.str,
|
|
});
|
|
|
|
// Grab some data...
|
|
const json = await fetch(...);
|
|
const data = JSON.parse(data);
|
|
|
|
// Assert the data matches the User type.
|
|
try {
|
|
const user = User.assert(data);
|
|
} catch(e) {
|
|
console.log(`Data ${data} did not match the User type`);
|
|
console.log(`It failed with the following error: ${e}`);
|
|
}
|
|
```
|
|
|
|
Structural's type system strives to support every feature of TypeScript's
|
|
compile-time type system, but at runtime. This includes support for the
|
|
following advanced features:
|
|
|
|
* __Generics.__
|
|
* __Null safety:__ if you say something is a string, it will never be `null` or
|
|
`undefined`.
|
|
* __Structural subtyping:__ if `Person` records are defined by having a `name`,
|
|
an object with both a `name` and an `eyeColor` is a valid `Person`.
|
|
* __Algebraic data types:__ use `.and` and `.or` on types to compose them via
|
|
type intersections or unions.
|
|
* __Partial types:__ use `t.partial(...)` for an equivalent to `Partial<T>`,
|
|
and `t.deepPartial(...)` to make all nested types `Partial` as well.
|
|
|
|
|
|
## TypeScript integration
|
|
|
|
Structural is written in TypeScript and supports simple, transparent
|
|
compile-time type inference. You'll never have to write both a TypeScript type
|
|
and a Structural type: any Structural type will get automatically inferred into
|
|
a TypeScript type. For example:
|
|
|
|
```typescript
|
|
const User = t.subtype({
|
|
id: t.num,
|
|
name: t.str,
|
|
});
|
|
|
|
/*
|
|
In the following code, the `user` variable is automatically inferred to have
|
|
the following TypeScript type:
|
|
|
|
{
|
|
id: number,
|
|
name: string,
|
|
}
|
|
|
|
*/
|
|
const user = User.assert(data);
|
|
|
|
/*
|
|
* You can get a reference to the inferred type for Users using the following
|
|
* type helper:
|
|
*/
|
|
type UserType = t.GetType<typeof User>;
|
|
|
|
// This allows you to write typed function that operate on users like so:
|
|
function update(user: UserType) {
|
|
// ...
|
|
}
|
|
```
|
|
|
|
You can even generate TypeScript types as source code from Structural types,
|
|
[as explained later in the docs](#generating-typescript).
|
|
|
|
## Comparisons with other frameworks
|
|
|
|
Let's compare a longer, more realistic sample of user validation code to the
|
|
equivalent JSON Schema:
|
|
|
|
#### Structural:
|
|
```typescript
|
|
const User = t.subtype({
|
|
id: t.num,
|
|
name: t.str,
|
|
login: t.str,
|
|
hireable: t.bool,
|
|
});
|
|
```
|
|
|
|
And in six lines, you're done. And for TypeScript users, you'll never need to
|
|
write the type out again in the rest of your code: it's automatically inferred.
|
|
|
|
#### JSON Schema:
|
|
```
|
|
{
|
|
"$id": "https://example.com/user.schema.json",
|
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
"title": "User",
|
|
"type": "object",
|
|
"properties": {
|
|
"id": {
|
|
"type": "number",
|
|
},
|
|
"name": {
|
|
"type": "string",
|
|
},
|
|
"login": {
|
|
"type": "string",
|
|
},
|
|
"hireable": {
|
|
"type": "boolean",
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Clocking in at 19 lines of code, it's over 3x more verbose than the equivalent
|
|
Structural validation. And for TypeScript users, JSON Schema is even worse!
|
|
You'll also need the following redundant type declaration somewhere in your
|
|
source files:
|
|
|
|
```typescript
|
|
type UserType = {
|
|
id: number,
|
|
name: string,
|
|
login: string,
|
|
hirable: boolean,
|
|
}
|
|
```
|
|
|
|
And every time you update the JSON Schema, you'll need to keep the type in
|
|
sync, since it can't be inferred at compile time.
|
|
|
|
If you really need JSON Schema -- for example, if you're integrating with
|
|
external systems not written in JavaScript or TypeScript -- you can generate
|
|
JSON Schema from Structural types in a single line of code:
|
|
|
|
```typescript
|
|
toJSONSchema("User schema", User)
|
|
```
|
|
|
|
## Basic types
|
|
|
|
* `t.any`: corresponds to `any`
|
|
* `t.array(...)`: correspond to `Array<...>`
|
|
* `t.instanceOf(...)`: corresponds to an `instanceof` check
|
|
* `t.is(name, guard)`: corresponds to a guard function; e.g. `t.is("bird", function
|
|
isBird(val: any): is Bird { ... })` would result in a Structural type that
|
|
runs the `isBird` function to determine whether a value is a `Bird`.
|
|
* `t.map(key, value)` corresponds to `Map<Key, Value>`
|
|
* `t.never` corresponds to `never`
|
|
* `t.num` corresponds to `number`
|
|
* `t.bigint` corresponds to `bigint`
|
|
* `t.str` corresponds to `string`
|
|
* `t.bool` corresponds to `boolean`
|
|
* `t.fn` corresponds to `Function`
|
|
* `t.sym` corresponds to `Symbol`
|
|
* `t.undef` corresponds to `undefined`
|
|
* `t.nil` corresponds to `null`
|
|
* `t.obj` corresponds to `Object`
|
|
* `t.maybe(type)` corresponds to `type | null`
|
|
* `t.set(value)` corresponds to `Set<Value>`
|
|
* `t.value(literal)` corresponds to literal type values, e.g. `type Hello =
|
|
"hello"` would be written as `t.value("hello")`
|
|
|
|
## Subtypes and exact types
|
|
|
|
Subtypes and exact types are how Structural implements structural types:
|
|
`t.subtype` defines a subtype, i.e. anything that has *at least* the keys
|
|
passes, whereas `t.exact` defines an exact type, i.e. the keys must exactly
|
|
match and unknown keys aren't allowed. They use the same syntax:
|
|
|
|
```typescript
|
|
const UserSubtype = t.subtype({
|
|
id: t.str,
|
|
purchaseCount: t.num,
|
|
});
|
|
|
|
// Passes:
|
|
UserSubtype.assert({
|
|
id: "123",
|
|
purchaseCount: 0,
|
|
});
|
|
|
|
// Passes:
|
|
UserSubtype.assert({
|
|
id: "123",
|
|
purchaseCount: 0,
|
|
name: "Bobby",
|
|
});
|
|
|
|
|
|
const UserExact = t.exact({
|
|
id: t.str,
|
|
purchaseCount: t.num,
|
|
});
|
|
|
|
// Passes:
|
|
UserExact.assert({
|
|
id: "123",
|
|
purchaseCount: 0,
|
|
});
|
|
|
|
// Fails:
|
|
UserExact.assert({
|
|
id: "123",
|
|
purchaseCount: 0,
|
|
name: "Bobby",
|
|
});
|
|
```
|
|
|
|
## Advanced type system features
|
|
|
|
Here's a more advanced example, showing how to compose types using type algebra
|
|
(`or` and `and`):
|
|
|
|
```typescript
|
|
import { t } from "structural";
|
|
|
|
const Person = t.subtype({
|
|
name: t.str,
|
|
});
|
|
|
|
const HasJob = t.subtype({
|
|
employer: t.str,
|
|
job: t.subtype({
|
|
role: t.str,
|
|
}),
|
|
});
|
|
|
|
const HasSchool = t.subtype({
|
|
school: t.str,
|
|
});
|
|
|
|
const Intern = Person.and(HasJob).and(HasSchool);
|
|
|
|
// Grab some data...
|
|
const json = await fetch(...);
|
|
const data = JSON.parse(json);
|
|
|
|
/*
|
|
Assert the data matches the Intern type. For TypeScript users,
|
|
the resulting `intern` variable is automatically inferred to
|
|
have the type:
|
|
|
|
{
|
|
name: string,
|
|
employer: string,
|
|
job: {
|
|
role: string,
|
|
},
|
|
school: string,
|
|
}
|
|
|
|
If the asssertion fails, an error is thrown.
|
|
*/
|
|
try {
|
|
const intern = Intern.assert(data);
|
|
} catch(e) {
|
|
console.log(`Data ${data} did not match the Intern type`);
|
|
}
|
|
```
|
|
|
|
## Custom validations
|
|
|
|
Structural supports writing custom validation functions that check values at
|
|
runtime. Functions should return true if the check passes, and false otherwise.
|
|
|
|
```typescript
|
|
import { t } from "structural";
|
|
|
|
const NonZeroNumber = t.num.validate(num => num !== 0);
|
|
|
|
// Passes:
|
|
NonZeroNumber.assert(1);
|
|
|
|
// Raises an error:
|
|
NonZeroNumber.assert(0);
|
|
```
|
|
|
|
## Slicing keys
|
|
|
|
By default, `assert` is zero-copy: the data you give it is the data that gets
|
|
returned. This means, for example, if you have the type:
|
|
|
|
```typescript
|
|
const Person = t.subtype({
|
|
name: t.str,
|
|
});
|
|
```
|
|
|
|
And you give it the following data:
|
|
|
|
```typescript
|
|
const validated = Person.assert({
|
|
name: "Matt",
|
|
eyeColor: "green",
|
|
});
|
|
```
|
|
|
|
Then `validated` will be exactly the data you passed in:
|
|
|
|
```typescript
|
|
{
|
|
name: "Matt",
|
|
eyeColor: "green",
|
|
}
|
|
```
|
|
|
|
(Although if you're using TypeScript, the type system will rightfully prevent
|
|
you from accessing `eyeColor`, because you didn't declare it as part of the
|
|
type.)
|
|
|
|
This behavior is useful when you want to preserve the original data that was
|
|
passed in, or if you don't care about preserving it but want to avoid
|
|
unnecessary allocations. If you want to make sure `validated` only contains
|
|
exactly the data described in `Person`, though -- and you don't want to use an
|
|
`exact` type, because you don't want to fail on unknown keys -- Structural also
|
|
provides a `slice` method that is equivalent to `assert`, but makes sure to
|
|
only return data with the known keys described by the type. For example:
|
|
|
|
```typescript
|
|
const sliced = Person.slice({
|
|
name: "Matt",
|
|
eyeColor: "green",
|
|
});
|
|
|
|
/*
|
|
The contents of `sliced` are:
|
|
|
|
{
|
|
name: "Matt",
|
|
}
|
|
|
|
because `eyeColor` was not defined in the Person type
|
|
*/
|
|
```
|
|
|
|
The `slice` call can be useful when you're calling third-party APIs and only
|
|
care about a few fields, and then intend to store the returned data. With
|
|
`assert`, you'd store the entire returned object, which would waste space in
|
|
your data store; with `slice`, you'll only end up storing the data you care
|
|
about.
|
|
|
|
The `slice` method exists on all types, even ones without keys, so you can
|
|
safely drop it in to replace `assert` calls. For types that don't have keys,
|
|
like `t.num`, `slice` is an alias to `assert`; similarly, for types that may
|
|
have keys but don't track them in the type, like `t.obj` (which accepts any
|
|
object), `slice` is also an alias to `assert` since we don't know which keys to
|
|
slice out.
|
|
|
|
Call to `slice` work even through the algebraic types created with `.and` and
|
|
`.or`; for example:
|
|
|
|
```typescript
|
|
import { t } from "structural";
|
|
|
|
const Person = t.subtype({
|
|
name: t.str,
|
|
});
|
|
|
|
const HasJob = t.subtype({
|
|
employer: t.str,
|
|
job: t.subtype({
|
|
role: t.str,
|
|
}),
|
|
});
|
|
|
|
const HasSchool = t.subtype({
|
|
school: t.str,
|
|
});
|
|
|
|
const Intern = Person.and(HasJob).and(HasSchool);
|
|
|
|
const sliced = Intern.slice({
|
|
name: "Jenkins",
|
|
employer: "Mr. Walburn",
|
|
job: {
|
|
role: "Coffee fetcher",
|
|
},
|
|
alive: false,
|
|
});
|
|
|
|
/*
|
|
The contents of `sliced` are:
|
|
|
|
{
|
|
name: "Jenkins",
|
|
employer: "Mr. Walburn",
|
|
job: {
|
|
role: "Coffee fetcher",
|
|
},
|
|
}
|
|
|
|
because `alive` wasn't defined in the Intern type.
|
|
*/
|
|
```
|
|
|
|
## Generating TypeScript
|
|
|
|
You can automatically generate valid TypeScript as source code strings from
|
|
Structural types with the `toTypescript` function. For example:
|
|
|
|
```typescript
|
|
import { toTypescript, t } from "structural";
|
|
|
|
const ts = toTypescript(t.subtype({
|
|
id: t.num,
|
|
}));
|
|
```
|
|
|
|
The `ts` string would be:
|
|
|
|
```typescript
|
|
{
|
|
id: number,
|
|
}
|
|
```
|
|
|
|
You can also generate TypeScript type definitions with type names by passing the
|
|
Structral types in as a hash; for example:
|
|
|
|
```typescript
|
|
const User = t.subtype({
|
|
id: t.num,
|
|
});
|
|
|
|
toTypescript({ User });
|
|
```
|
|
|
|
Which generates:
|
|
|
|
```typescript
|
|
type User = {
|
|
id: number,
|
|
};
|
|
```
|
|
|
|
If you pass multiple types into the hash, the string will contain all of the
|
|
types in the order they appeared in the hash; for example:
|
|
|
|
```typescript
|
|
const Customer = t.subtype({
|
|
orders: t.num,
|
|
});
|
|
const Business = t.subtype({
|
|
customers: t.array(Customer),
|
|
});
|
|
|
|
|
|
toTypescript({ Customer, Business });
|
|
```
|
|
|
|
Generates:
|
|
|
|
```typescript
|
|
type Customer = {
|
|
orders: number,
|
|
};
|
|
|
|
type Business = {
|
|
customers: Array<Customer>,
|
|
};
|
|
```
|
|
|
|
### Comments
|
|
|
|
Structural provides some convenience methods for generating good TypeScript
|
|
code, allowing you to add comments to the code you generate. The comment
|
|
methods are no-ops at runtime, but help readability for your generated
|
|
TypeScript. Here's an example of a comment:
|
|
|
|
```typescript
|
|
const User = t.subtype({
|
|
name: t.str.comment("The user's full name"),
|
|
});
|
|
```
|
|
|
|
Running `toTypescript({ User })` on that struct would generate:
|
|
|
|
```typescript
|
|
type User = {
|
|
// The user's full name
|
|
name: string,
|
|
};
|
|
```
|
|
|
|
Multiline comments are also supported and have generally-sensible output
|
|
formatting:
|
|
|
|
```typescript
|
|
t.subtype({
|
|
bar: t.str.comment(`
|
|
A multi-line comment.
|
|
It documents the bar field.
|
|
`),
|
|
});
|
|
```
|
|
|
|
Which would be generated as:
|
|
|
|
```typescript
|
|
{
|
|
/*
|
|
* A multi-line comment.
|
|
* It documents the bar field.
|
|
*/
|
|
bar: string,
|
|
}
|
|
```
|
|
|
|
### Renaming keys in dictionaries
|
|
|
|
By default, the `dict` type will name its keys `key`, like so:
|
|
|
|
```typescript
|
|
const OrderCount = t.dict(t.num);
|
|
toTypescript({ OrderCount });
|
|
```
|
|
```typescript
|
|
type OrderCount = {[key: string]: number};
|
|
```
|
|
|
|
Depending on your dictionary, you may want to use a more meaningful name than
|
|
just `key`. For example, if you're mapping customer names to order counts, it
|
|
might be useful to have the key be named `customer` for readability:
|
|
|
|
```typescript
|
|
const OrderCount = t.dict(t.num).keyName("customer");
|
|
toTypescript({ OrderCount });
|
|
```
|
|
```typescript
|
|
type OrderCount = {[customer: string]: number};
|
|
```
|
|
|
|
### Readability for nested types
|
|
|
|
Generally, using `toTypescript({ ... })` just does the right thing in terms of
|
|
generating deeply-nested type data for multiple Structural types that reference
|
|
each other. However, if you only want to generate a *single* one of the types,
|
|
you'll quickly realize that the generated TypeScript is less than ideal in
|
|
terms of readability: while it's technically syntactically correct, it
|
|
duplicates the structural type definitions for the referenced types; for
|
|
example:
|
|
|
|
```typescript
|
|
const Customer = t.subtype({
|
|
orders: t.num,
|
|
});
|
|
const Business = t.subtype({
|
|
customers: t.array(Customer),
|
|
});
|
|
|
|
const businessTs = toTypescript(Business);
|
|
```
|
|
|
|
This would generate the following two type definitions:
|
|
|
|
```typescript
|
|
{
|
|
customers: Array<{
|
|
orders: number,
|
|
}>,
|
|
}
|
|
```
|
|
|
|
While that's technically *correct*, you might want to just reference the
|
|
`Customer` class if you've defined it elsewhere. For example, it might be nice
|
|
to generate the following:
|
|
|
|
```typescript
|
|
{
|
|
customers: Array<Customer>,
|
|
}
|
|
```
|
|
|
|
With `toTypescript`, that's pretty easy to do if you want to generate both
|
|
Customer and Business. Instead of passing in a single type and assigning it to
|
|
a type name, you can instead just pass in all the types in a hash, and it'll
|
|
de-duplicate everything for you and assign them type names:
|
|
|
|
```typescript
|
|
toTypescript({ Customer, Business });
|
|
```
|
|
|
|
```typescript
|
|
type Customer = {
|
|
id: number,
|
|
};
|
|
|
|
type Business = {
|
|
customers: Array<Customer>,
|
|
};
|
|
```
|
|
|
|
But if you only want Business, what to do? Well, you can use the extra options
|
|
to `toTypescript` that the hash version is a wrapper over.
|
|
|
|
#### `useReference`
|
|
|
|
The `useReference` option helps readability of deeply-nested types. Using the
|
|
example of `Customer` and `Business` Structral types from above, we can use
|
|
`useReference` to ensure that when we generate the `Business` type, it replaces
|
|
references to `Customer` with the id `Customer`, rather than re-generating the
|
|
entire structural type for `Customer` inline. For example:
|
|
|
|
```typescript
|
|
const Customer = t.subtype({
|
|
orders: t.num,
|
|
});
|
|
const Business = t.subtype({
|
|
customers: t.array(Customer),
|
|
});
|
|
|
|
const businessTs = toTypescript(Business, {
|
|
useReference: {
|
|
Customer,
|
|
},
|
|
});
|
|
```
|
|
|
|
Any value in the `useReference` hash will be replaced in the TypeScript output
|
|
with the key name. In this case, we're replacing `Customer` with `"Customer"`
|
|
(and using object shorthand syntax to make that relatively ergonomic). The
|
|
`businessTs` string would be:
|
|
|
|
```typescript
|
|
{
|
|
customers: Array<Customer>,
|
|
}
|
|
```
|
|
|
|
#### `assignToType`
|
|
|
|
The `assignToType` option auto-generates the syntax to assign a type a name,
|
|
and inserting a semicolon after the type definition. For example:
|
|
|
|
```typescript
|
|
const ts = toTypescript(t.num.or(t.str), {
|
|
assignToType: "id",
|
|
});
|
|
```
|
|
|
|
This would result in `ts` having the following value:
|
|
|
|
```typescript
|
|
type id = number
|
|
| string;
|
|
```
|
|
|
|
## Generating JSON Schema
|
|
|
|
For interop with other languages or APIs, rather than writing JSON Schema by
|
|
hand, you can instead write Structural types and generate the JSON Schema using
|
|
the `toJSONSchema` function:
|
|
|
|
```typescript
|
|
import { toJSONSchema, t } from "structural";
|
|
|
|
const User = t.subtype({
|
|
name: t.str,
|
|
});
|
|
|
|
const schema = toJSONSchema("User", User);
|
|
// Generates:
|
|
{
|
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
title: "User",
|
|
type: "object",
|
|
required: [ "name" ],
|
|
properties: {
|
|
name: { type: "string" },
|
|
},
|
|
}
|
|
```
|
|
|
|
The `$schema` and `title` fields only appear at the top level of the generated
|
|
schema; here's what a nested type would look like:
|
|
|
|
```typescript
|
|
import { toJSONSchema, t } from "structural";
|
|
|
|
const Pet = t.value("dog").or(t.value("cat"));
|
|
const User = t.subtype({
|
|
name: t.str,
|
|
pet: t.optional(Pet),
|
|
});
|
|
|
|
const schema = toJSONSchema("User", User);
|
|
// Generates:
|
|
{
|
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
title: "User",
|
|
type: "object",
|
|
required: [ "name" ],
|
|
properties: {
|
|
name: { type: "string" },
|
|
pet: {
|
|
enum: [ "dog", "cat" ],
|
|
},
|
|
},
|
|
}
|
|
```
|
|
|
|
Unions will either generate JSON Schema enums (if all of the union members are
|
|
values), or `anyOf` types. Intersections will generate `allOf` types.
|
|
|
|
### Unsupported types in JSON Schema
|
|
|
|
Attempting to convert non-JSON types into JSON Schema will throw an error; for
|
|
example, Sets, Maps, functions, and `undefined` will throw errors. By default,
|
|
the following will throw errors, but can be optionally converted into
|
|
`description` keys by passing in options:
|
|
|
|
* Is (set `errorOnIs: false`)
|
|
* Validation (set `errorOnValidations: false`)
|
|
|
|
By default, `never` will also error. Setting `errorOnNever: false` will convert
|
|
`never` into impossible JSON Schemas, but if you do that, it will be impossible
|
|
for anyone to send you valid JSON of that schema.
|
|
|
|
### Comments
|
|
|
|
Structural `.comment` annotations will be converted into `description` keys.
|
|
For example:
|
|
|
|
```typescript
|
|
import { toJSONSchema, t } from "structural";
|
|
|
|
const User = t.subtype({
|
|
name: t.str.comment("The user's full name"),
|
|
});
|
|
|
|
const schema = toJSONSchema("User", User);
|
|
// Generates:
|
|
{
|
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
title: "User",
|
|
type: "object",
|
|
required: [ "name" ],
|
|
properties: {
|
|
name: {
|
|
type: "string",
|
|
description: "The user's full name",
|
|
},
|
|
},
|
|
}
|
|
```
|