TypeSpec for the OpenAPI developer
This guide is an introduction to TypeSpec using concepts that will be familiar to developers that either build or use API definitions in OpenAPI v2 or v3.
In many cases, this will also describe how the typespec-autorest and openapi3 emitters translate TypeSpec designs into OpenAPI.
The document is organized around the features of an OpenAPI v2 or v3 definition. The idea is that if you know how to describe some API feature in OpenAPI, you can just navigate to the section of this document for that feature.
Data Types
In OpenAPI v2/v3, data types are specified using the type
and format
fields in a schema.
The TypeSpec equivalent of OpenAPI data types are the TypeSpec primitive types or built-in models.
type and format
The following table shows how common OpenAPI types map to TypeSpec types:
OpenAPI type /format | TypeSpec type | Notes |
---|---|---|
type: integer, format: int32 | int32 | |
type: integer, format: int64 | int64 | |
type: number, format: float | float32 | |
type: number, format: double | float64 | |
type: string | string | |
type: string, format: byte | bytes | for content-type == 'application/json' or 'text/plain' |
type: string, format: binary | bytes | for "binary" content types, e.g. 'application/octet-stream', 'image/jpeg' |
type: boolean | boolean | |
type: string, format: date | plainDate | |
type: string, format: date-time | utcDateTime | RFC 3339 date in coordinated universal time (UTC) |
type: string, format: date-time | offsetDateTime | RFC 3339 date with offset |
type: string, format: password | @secret string |
You can also define a property with no type specified using the TypeSpec unknown
type.
@doc("This property has no `type` defined.")
noType?: unknown;
OpenAPI allows any string as a format, and there is a registry of common formats. TypeSpec supports some of these directly.
OpenAPI type /format | TypeSpec type | Notes |
---|---|---|
type: number, format: decimal | decimal | |
type: integer, format: int8 | int8 | |
type: integer, format: int16 | int16 | |
type: integer, format: uint8 | uint8 | |
type: string, format: uri | url |
For formats that are not supported directly, you can use the built-in @format
decorator to specify
the format explicitly.
JSON Schema assertions
OpenAPI supports a variety of "assertions" that can be used further restrict the values allowed for a data type. These are actually borrowed into OpenAPI from JSON Schema.
For type: integer
and type: number
data types:
OpenAPI/JSON Schema keyword | TypeSpec construct | Notes |
---|---|---|
minimum: value | @minValue(value) decorator | |
maximum: value | @maxValue(value) decorator |
For type: string
data types:
OpenAPI/JSON Schema keyword | TypeSpec construct | Notes |
---|---|---|
minLength: value | @minLength(value) decorator | |
maxLength: value | @maxLength(value) decorator | |
pattern: regex | @pattern(regex) decorator |
For type: array
data types:
OpenAPI/JSON Schema keyword | TypeSpec construct | Notes |
---|---|---|
minItems: value | @minItems(value) decorator | |
maxItems: value | @maxItems(value) decorator |
enum
There are two ways to define an enum
data type. One is with the TypeSpec enum
statement, e.g.:
enum Color {
"red",
"blue",
"green",
}
Another is to use the union operation to define the enum values inline, e.g.:
size?: "small" | "medium" | "large" | "x-large";
default
A model property that specifies a default value using "=" will produce a default
field in the schema for this property.
answer?: int32 = 42;
color?: string = "purple";
produces
answer:
type: integer
format: int32
default: 42
color:
type: string
default: purple
Host / BasePath / Servers
In OpenAPI v2, the host
and basePath
fields at the top-level of the API definition combine to form the base URL for the API. The paths defined in the paths
object are appended to this base URL to form the absolute URL for an operation.
In OpenAPI v3, the top-level servers
field specifies an array of server
objects [v3] with a base URL, which may be parameterized, to which the operation path is appended.
In TypeSpec, the host
in OpenAPI v2 or servers
in OpenAPI v3 can be specified with the @server
decorator
on the namespace (from @typespec/http
library). You can use this decorator multiple times to specify multiple servers.
Paths Object
In OpenAPI, the paths
object [v2, v3] is the top-level structure for defining the operations of the API, organized with the "path" for the operation.
In TypeSpec, you can specify the path for a namespace, interface, or operation using the @route
decorator.
When the value of the @route
decorator contains path parameters, operations within the scope of the decorator
must declare parameters with the same name and type. If an operation declares a path parameter that is not present
in the route, this defines a new path that is the value from the @route
decorator with the path parameter appended.
@route("/pets")
namespace Pets {
op create(@body pet: Pet): Pet; // uses path "/pets"
op read(@path petId: int32): Pet; // uses path "/pets/{petId}"
}
When the @route
decorator is used within a namespace or interface that also has a @route
decorator, the path is
obtained by concatenating the routes.
@route("/widgets")
namespace Widgets {
// widgets operations
@route("/{id}/parts")
namespace Parts {
op list(@path id: string): Part[] | Error; // uses path "/widgets/{id}/parts"
}
}
Path Item Object
In OpenAPI, a path item object [v2, v3] describes the operations available on a single path. A path may have at most one get
, put
, post
, patch
, delete
, or head
operation.
In TypeSpec, operations are defined within a namespace or interface with a syntax similar to typescript functions.
The HTTP method for an operation can be specified explicitly using a decorator: @get
, @put
, @post
, @patch
, @delete
, or @head
.
If an HTTP method decorator is not specified then the default is post
if there is a body and get
otherwise.
@tag("Gadgets")
@route("/gadgets")
namespace Gadgets {
op create(@body gadget: Gadget): Gadget | Error; // uses "post" method
op read(@path id: string): Gadget | Error; // uses "get" method
}
Other path item fields:
OpenAPI pathItem field | TypeSpec construct | Notes |
---|---|---|
summary | Not currently supported. | |
description | Not currently supported. | |
parameters | Not currently supported. |
Operation Object
In OpenAPI, an operation object [v2, v3] describes an operation.
The fields in an OpenAPI operation object are specified with the following TypeSpec constructs:
OpenAPI operation field | TypeSpec construct | Notes |
---|---|---|
tags | @tag decorator | |
summary | @summary decorator | |
description | @doc decorator or doc comment | |
externalDocs | @externalDocs decorator | |
operationId | operation name or @operationId decorator | |
parameters | op parameter list | see Parameter Object |
requestBody | parameter with @body decorator | see Request Body Object |
responses | op return type(s) | see Responses Object |
callbacks | Not currently supported. | |
deprecated | @deprecated decorator | |
security | Not currently supported. | |
servers | @server decorator | Can be specified multiple times. |
Tags
Tags can be specified using the @tag
decorator on an operation.
The @tag
decorator can also be used on a namespace or interface to specify tags for all operations within the namespace or interface.
Tags are additive, so tags specified on an operation are added to the tags specified on the namespace or interface.
The @tag
decorator can be used multiple times to specify multiple tags on an operation, namespace, or interface.
Description
Use the @doc
decorator to specify the description for an operation. The value of the @doc
decorator can be a multi-line string
and can contain markdown formatting.
@doc("""
Get status info for the service.
The status includes the current version of the service.
The status value may be one of:
- `ok`: the service is operating normally
- `degraded`: the service is operating in a degraded state
- `down`: the service is not operating
""")
@tag("Status")
@route("/status")
@get
op status(): string;
You can also use a "doc comment" to specify the description for an operation. A doc comment is a comment that begins with /**
.
Doc comments may be spread across multiple lines and may contain markdown formatting.
/**
* Get health info for the service.
* The health includes the current version of the service.
* The health value may be one of:
* - `ok`: the service is operating normally
* - `degraded`: the service is operating in a degraded state
* - `down`: the service is not operating
*/
@tag("Health")
@route("/health")
@get
op health(): string;
operationId
You can specify the operationId for an operation using the @operationId
decorator.
When the @operationId
decorator is not specified, the operationId is generated from the operation name.
For an operation defined in the top-level namespace, the operationId is the just operation name.
If the operation is defined within a inner namespace or interface, then the operationId is
prefixed with the name of the innermost namespace or interface name.
Note: this approach will generally produce unique operationIds, as required by OpenAPI, but it is possible to create duplicate operationIds.
Parameter Object
In OpenAPI, a parameter object [v2, v3] describes a single operation parameter.
The following fields of a parameter object are common to both OpenAPI v2 and v3:
OpenAPI parameter field | TypeSpec construct | Notes |
---|---|---|
name | parameter name | |
in | decorator | @query , @path , @header , @body |
description | @doc decorator | |
required | from parameter "optionality" | a "?" following the parameter name indicates it is optional (required: false ), otherwise it is required (required: true ) |
allowEmptyValue | Not currently supported. |
OpenAPI v2
The following fields of a parameter object are specific to OpenAPI v2:
OpenAPI v2 parameter field | TypeSpec construct | Notes |
---|---|---|
type | parameter type | see Data Types |
collectionFormat | format parameter on @query or @header |
Collection Formats
In OpenAPI v2, the collectionFormat
field of a query or header parameter object specifies how multiple values are delimited.
You can use the format
field of the @query
or @header
decorator to specify the collection format.
@get read(
@path id: string,
@query({format: "csv"}) csv?: string[], // has collectionFormat: "csv"
@query({format: "multi"}) multi?: string[], // has collectionFormat: "multi"
): Widget | Error;
OpenAPI v3
The following fields of a parameter object are specific to OpenAPI v3:
OpenAPI v3 parameter field | TypeSpec construct | Notes |
---|---|---|
style | format parameter on @query or @header | |
explode | format parameter on @query or @header | |
schema | parameter schema | see Schema Object |
deprecated | Not currently supported. | |
example | Not currently supported. | |
examples | Not currently supported. | |
content | Not currently supported. |
Request Body Object (OAS3)
In OpenAPI v3, the operation request body is defined with a Request Body object rather than as a parameter.
An OpenAPI v3 Request Body object corresponds to a TypeSpec op
parameter with the @body
decorator.
OpenAPI requestBody field | TypeSpec construct | Notes |
---|---|---|
description | @doc decorator | |
required | parameter "optionality" | a "?" following the parameter name indicates it is optional (required: false ), otherwise it is required (required: true ) |
content | @body parameter type |
The media type of the request body is specified with a content-type
header. If content-type
has multiple values
then content
will have one entry for each value.
@put op uploadImage(@header contentType: "image/png", @body image: bytes): void;
@post op analyze(
@header contentType: "application/octet-stream" | "application/pdf" | "image/jpeg",
@body image: bytes,
): string | Error;
To get multiple content
entries with different schemas (say one structured and one binary),
you need to define two separate operations that share the same path and method.
You do with with the @sharedRoute
decorator.
@route(":process")
namespace Process {
@sharedRoute
@post
op process(...Widget): Widget | Error;
model CsvBody {
@header contentType: "text/csv";
@body _: string;
}
@sharedRoute
@post
op process2(...CsvBody): Widget | Error;
}
Responses Object
In OpenAPI, the responses object [v2, v3] specifies the possible responses for an operation. The responses object maps a HTTP response code to the expected response.
In TypeSpec, operation responses are defined by the return types of the op
. The status code for a response can be specified as a property in the return type with the @statusCode
decorator. The value of the property with the @statusCode
decorator should be an HTTP status code or union of status codes. When the value is a union of status codes, a response is generated for each status code in the union.
If a return type does not contain a statusCode
, the default is 200
except for void
which defaults to 204
.
To get the default
response, specify the @error
decorator on the return type model.
@get op read(@path id: string): Widget; // has "200" response
@delete op delete(@path id: string): void; // has "204" response
// has "200" and "201" response
@put op create(@body widget: Widget): {
@statusCode _: "200" | "201";
@body body: Widget;
};
// has "200" and "default" response
@post op update(@body widget: Widget): Widget | Error;
The TypeSpec.Http package also defines several standard response types.
HTTP Status Code | TypeSpec construct |
---|---|
200 | OkResponse |
201 | CreatedResponse |
202 | AcceptedResponse |
204 | NoContentResponse |
301 | MovedResponse |
304 | NotModifiedResponse |
401 | UnauthorizedResponse |
404 | NotFoundResponse |
409 | ConflictResponse |
You can intersect these standard response types with your own response types.
// has "200", '409', and "default" responses
@post op update(@body widget: Widget): Widget | (ConflictResponse & Error) | Error;
Response Object
In OpenAPI, a response object [v2, v3] describes a single response for an operation. The structure of the response object changed significantly from OpenAPI v2 to OpenAPI v3, but there are many elements common to both.
The fields in an OpenAPI response object are specified with the following TypeSpec constructs:
OpenAPI response field | TypeSpec construct | Notes |
---|---|---|
description | @doc decorator | |
headers | fields in the return type with @header decorator | required or optional based on optionality of field |
schema (OAS2) | return type or type of `@body`` property | |
content (OAS3) | return type or type of `@body`` property | |
examples (OAS3) | Not currently supported. | |
links (OAS3) | Not currently supported. |
@get op read(@path id: string): {
@doc("the widget")
@body
widget: Widget;
@header xRateLimitRemaining: number;
@header xRateLimitReset: number;
};
The media type of the request body is specified with a content-type
header. If content-type
has multiple values
then content
will have one entry for each value.
To get multiple content
entries with different schemas, use a union type.
@tag("Response Content")
@route("/response-content")
namespace ResponseContent {
@get op read(@path id: string): Widget | {
@header contentType: "text/html";
@body _: string;
} | {
@header contentType: "image/jpeg";
@body _: bytes;
};
}
Schema Object
OpenAPI schemas are represented in TypeSpec by models. Models have any number of members and can extend and be composed with other models.
Models can be defined with the model
statement and then referenced by name, which generally results in a $ref
to a schema for the model in the definitions
or components.schemas
section of the OpenAPI document.
TypeSpec supports the "spread" operator (...
), which copies the members of the source model into the target model.
But TypeSpec processes all spread transformations before emitters are invoked, so this form of reuse is not represented in the emitted OpenAPI.
The spread operation is useful if you want one or more properties to be present in several different models but in a standard fashion. For example:
model Legs {
@doc("number of legs") legs: int32;
}
model Dog {
name: string;
...Legs;
}
model Cat {
name: string;
...Legs;
}
model Snake {
name: string;
// snakes have no legs
}
additionalProperties
You can generate a schema with additionalProperties
with the TypeSpec Record
construct.
bar: Record<unknown>;
is produced as
bar:
type: object
additionalProperties: {}
To get a schema having both properties
and additionalProperties
, define a model that extends Record<unknown>
.
model Bar extends Record<unknown> {
bar?: string;
}
produces
Bar:
type: object
properties:
bar:
type: string
additionalProperties: {}
To define a schema with additionalProperties
that has a specific type, use the Record
construct with a type parameter.
bar: Record<string>;
results in
bar:
type: object
additionalProperties:
type: string
allOf and polymorphism using allOf
TypeSpec also supports single inheritance of models with the extends
keyword. This construct can be used to produce an allOf
with a single element (the parent schema) in OpenAPI. For example:
model Pet {
name: string;
}
model Cat extends Pet {
meow: int32;
}
model Dog extends Pet {
bark: string;
}
TypeSpec does not currently provide a means to produce an allOf
with more than one element -- these are generally treated as "composition" in code generators and thus better represented in TypeSpec with the spread operator.
Models with a @discriminator
decorator can be extended to produce polymorphic schemas in either OpenAPI v2 or v3 using allOf
.
This schema produced for the base model will be defined with a discriminator
property and schemas for the child models will allOf
the base schema and add additional properties.
For example:
@discriminator("kind")
model Pet {
name: string;
weight?: float32;
}
model Cat extends Pet {
kind: "cat";
meow?: int32;
}
model Dog extends Pet {
kind: "dog";
bark?: string;
}
generates:
Cat:
type: object
properties:
kind:
type: string
enum:
- cat
meow:
type: integer
format: int32
required:
- kind
allOf:
- $ref: '#/components/schemas/Pet'
Dog:
type: object
properties:
kind:
type: string
enum:
- dog
bark:
type: string
required:
- kind
allOf:
- $ref: '#/components/schemas/Pet'
Pet:
type: object
properties:
kind:
type: string
description: Discriminator property for Pet.
name:
type: string
weight:
type: number
format: float
discriminator:
propertyName: kind
mapping:
cat: '#/components/schemas/Cat'
dog: '#/components/schemas/Dog'
required:
- name
Polymorphism using anyOf and oneOf (OAS3)
Polymorphism can also be represented in OpenAPI v3 with anyOf
or oneOf
constructs.
These can be represented in TypeSpec with a union type.
union Pet {
cat: Cat,
dog: Dog,
}
model Cat {
meow?: int32;
}
model Dog {
bark?: string;
}
generates a Pet schema with anyOf
.
Pet:
anyOf:
- $ref: "#/components/schemas/Cat"
- $ref: "#/components/schemas/Dog"
The openapi emitter uses anyOf
by default because the schemas may not be mutually exclusive.
But the @oneOf
decorator of the OpenAPI library can be used to force the use of oneOf
instead.
import "@typespec/openapi3";
using OpenAPI;
@oneOf
union Pet {
cat: Cat,
dog: Dog,
}
produces:
Pet:
oneOf:
- $ref: "#/components/schemas/Cat"
- $ref: "#/components/schemas/Dog"
To make Pet a discriminated union, add the @discriminator
decorator and add the discriminator property
with a string literal value to each of the child schemas.
@discriminator("kind")
@oneOf
union Pet {
cat: Cat,
dog: Dog,
}
model Cat {
kind: "cat";
meow?: int32;
}
model Dog {
kind: "dog";
bark?: string;
}
results in the following schema for Pet:
Pet:
oneOf:
- $ref: "#/components/schemas/Cat"
- $ref: "#/components/schemas/Dog"
discriminator:
propertyName: kind
mapping:
cat: "#/components/schemas/Cat"
dog: "#/components/schemas/Dog"
definitions / components
OpenAPI supports reuse of schemas, parameters, responses, and other elements with the definitions
(OAS2) or components
(OAS3) section of an OpenAPI definition.
Referencing a model by name (not with "spread"), as an op
parameter or return type or as the type of a property in another model, generally results in a $ref
to a schema for the model in the definitions
or components.schemas
section of the OpenAPI document.
Reusable parameters can be defined as members of a model and then incorporated into an operation parameter list using the spread operator. For example:
model PetId {
@path petId: int32;
}
namespace Pets {
op read(...PetId): Pet | Error;
}
results in a $ref
to the named parameter PetId
in either parameters
or components.parameters
.
Info Object
In OpenAPI, the info
object [v2, v3] contains metadata about the API such as a title
, description
, license
, and version
.
In TypeSpec this information is specified with decorators on the namespace.
OpenAPI info field | TypeSpec decorator | Notes |
---|---|---|
title | @service({title: } | TypeSpec built-in decorator |
version | @service({version: } | TypeSpec built-in decorator |
description | @doc | TypeSpec built-in decorator |
license | @info | |
contact | @info |
@doc("The Contoso Widget Service provides access to the Contoso Widget API.")
@service({
title: "Widget Service",
})
@info({
contact: {
name: "API Support",
email: "contact@contoso.com",
},
license: {
name: "Apache 2.0",
url: "https://www.apache.org/licenses/LICENSE-2.0.html",
},
})
namespace DemoService;
Consumes / Produces (OAS2)
In OpenAPI v2, the top-level consumes
and produces
fields specify a list of MIME types an operation can consume / produce
when not overridden by a consumes
or produces
on an individual operation.
The typespec-autorest emitter previously supported @produces
and @consumes
decorators on a namespace, but these are deprecated
in favor of explicit content-type
and accept
header properties in request and response bodies.
securityDefinitions / securitySchemes Object
Use @useAuth
decorator from the `@typespec/rest" library
using TypeSpec.Http;
@useAuth(OAuth2Auth<["read", "write"]>)
namespace MyService;
Specification Extensions
You can add arbitrary specification extensions ("x-" properties) to a model or an operation with the @extension
decorator.
For example:
namespace Pets {
@extension("x-streaming-operation", true) op read(...PetId): Pet | Error;
}