OpenAPI
zio-blocks-openapi is a complete, type-safe OpenAPI 3.1 data model for building API documentation programmatically. It provides immutable case classes and sealed traits representing every OpenAPI concept—operations, parameters, security schemes, and components—enabling you to construct OpenAPI documents in compile-time-safe Scala and export them as JSON for consumption by tools like Swagger UI, Redoc, and API validators.
Core types: OpenAPI, Info, Paths, PathItem, Operation, Parameter, RequestBody, Response, Components, SchemaObject, SecurityScheme, ReferenceOr.
final case class OpenAPI(
openapi: String,
info: Info,
servers: Option[Chunk[Server]] = None,
paths: Option[Paths] = None,
components: Option[Components] = None,
security: Option[Chunk[SecurityRequirement]] = None
)
Introduction
OpenAPI documents are the lingua franca for API specifications. They define request/response contracts, authentication methods, and data schemas in a standardized JSON or YAML format that external tools consume. Building these documents manually in JSON is error-prone; maintaining them as your API evolves is tedious.
The OpenAPI module bridges the gap by letting you author API specs as Scala code—leveraging the type system for compile-time correctness—then export to standard JSON that any OpenAPI tool understands. You get type safety during authoring plus interoperability with the entire OpenAPI ecosystem.
Installation
libraryDependencies += "dev.zio" %% "zio-blocks-openapi" % "0.0.56"
// You'll also need the schema module for Schema[A] integration:
libraryDependencies += "dev.zio" %% "zio-blocks-schema" % "0.0.56"
For Scala.js:
libraryDependencies += "dev.zio" %%% "zio-blocks-openapi" % "0.0.56"
Supported Scala versions: 2.13.x and 3.x.
How They Work Together
The OpenAPI module follows a clear workflow:
1. Define your data types using ZIO Blocks Schema:
import zio.blocks.openapi._
import zio.blocks.docs._
import zio.blocks.chunk._
import zio.blocks.schema._
case class User(id: Int, name: String, email: String)
object User {
implicit val schema: Schema[User] = Schema.derived
}
case class ErrorResponse(code: Int, message: String)
object ErrorResponse {
implicit val schema: Schema[ErrorResponse] = Schema.derived
}
2. Create an OpenAPI document by composing types:
val api = OpenAPI(
openapi = "3.1.0",
info = Info(
title = "User API",
version = "1.0.0",
description = Some(md"API for managing users")
),
paths = Some(Paths(ChunkMap(
"/users" -> PathItem(
get = Some(Operation(
summary = Some(md"List all users"),
description = Some(md"Returns a paginated list of users"),
responses = Responses(ChunkMap(
"200" -> ReferenceOr.Value(Response(
description = md"Successful response",
content = ChunkMap(
"application/json" -> MediaType(
schema = Some(ReferenceOr.Value(
Schema[List[User]].toOpenAPISchema
))
)
)
))
))
))
),
"/users/{id}" -> PathItem(
get = Some(Operation(
summary = Some(md"Get a user by ID"),
parameters = Chunk(
ReferenceOr.Value(Parameter(
name = "id",
in = ParameterLocation.Path,
required = true,
schema = Some(ReferenceOr.Value(
Schema[Int].toOpenAPISchema
))
))
),
responses = Responses(ChunkMap(
"200" -> ReferenceOr.Value(Response(
description = md"User found",
content = ChunkMap(
"application/json" -> MediaType(
schema = Some(ReferenceOr.Value(
Schema[User].toOpenAPISchema
))
)
)
)),
"404" -> ReferenceOr.Value(Response(
description = md"User not found",
content = ChunkMap(
"application/json" -> MediaType(
schema = Some(ReferenceOr.Value(
Schema[ErrorResponse].toOpenAPISchema
))
)
)
))
))
))
)
))),
components = Some(Components(
schemas = ChunkMap(
Schema[User].toRefSchema._2._1 -> ReferenceOr.Value(Schema[User].toRefSchema._2._2),
Schema[ErrorResponse].toRefSchema._2._1 -> ReferenceOr.Value(Schema[ErrorResponse].toRefSchema._2._2)
)
))
)
3. Serialize to JSON for tools to consume:
import zio.blocks.openapi.OpenAPICodec._
val json = openAPICodec.encodeValue(api)
4. Render or serve the JSON (e.g., to Swagger UI):
import zio.blocks.schema.json._
val jsonString = Json.jsonCodec.encodeToString(json, WriterConfig.withIndentionStep2)
// jsonString: String = """{
// "openapi": "3.1.0",
// "info": {
// "title": "User API",
// "version": "1.0.0",
// "description": "API for managing users\n\n"
// },
// "paths": {
// "/users": {
// "get": {
// "responses": {
// "200": {
// "description": "Successful response\n\n",
// "content": {
// "application/json": {
// "schema": {
// "items": {
// "properties": {
// "id": {
// "type": "integer",
// "maximum": 2147483647,
// "minimum": -2147483648
// },
// "name": {
// "type": "string"
// },
// "email": {
// "type": "string"
// }
// },
// "type": "object",
// "required": [
// "id",
// "name",
// "email"
// ],
// "title": "User"
// },
// "type": [
// "array",
// "null"
// ],
// "title": "List[User]"
// }
// }
// }
// }
// },
// "summary": "List all users\n\n",
// ...
Type Relationships Diagram
OpenAPI (root document)
├─ info: Info (metadata)
├─ servers: Option[Chunk[Server]]
├─ paths: Option[Paths] (map of path strings to PathItem)
│ └─ PathItem
│ ├─ get: Operation
│ ├─ post: Operation
│ ├─ put: Operation
│ └ ─ ... (other HTTP methods)
│ ├─ parameters: Chunk[ReferenceOr[Parameter]]
│ ├─ requestBody: ReferenceOr[RequestBody]
│ │ └─ content: Map[String, MediaType]
│ │ └─ schema: ReferenceOr[SchemaObject]
│ └─ responses: Responses
│ └─ Map[statusCode, ReferenceOr[Response]]
│ └─ content: Map[String, MediaType]
│ └─ schema: ReferenceOr[SchemaObject]
├─ components: Option[Components]
│ ├─ schemas: ChunkMap[String, ReferenceOr[SchemaObject]]
│ ├─ responses: ChunkMap[String, ReferenceOr[Response]]
│ ├─ parameters: ChunkMap[String, ReferenceOr[Parameter]]
│ └─ securitySchemes: ChunkMap[String, ReferenceOr[SecurityScheme]]
└─ security: Option[Chunk[SecurityRequirement]]
Common Patterns
Building Reusable Schema Components
Avoid duplicating schema definitions by moving them to components.schemas:
import zio.blocks.openapi._
import zio.blocks.docs._
import zio.blocks.chunk._
import zio.blocks.schema._
case class User(id: Int, name: String, email: String)
object User {
implicit val schema: Schema[User] = Schema.derived
}
val userSchemaComponent = Schema[User].toRefSchema
// Returns: (ReferenceOr.Ref(...), ("User", SchemaObject(...)))
// Use the ref in operations, store the component in components.schemas
Using ReferenceOr for Inline vs. Referenced Schemas
ReferenceOr[A] is a sealed trait with two cases:
ReferenceOr.Ref: Points to a schema in#/components/schemas/<name>ReferenceOr.Value: Inline schema definition
Prefer Ref for reusable schemas; use Value for simple, one-off schemas:
import zio.blocks.openapi._
import zio.blocks.docs._
import zio.blocks.chunk._
import zio.blocks.schema._
// Reusable: use Ref
val userRef = ReferenceOr.Ref(Reference(`$ref` = "#/components/schemas/User"))
// One-off: use Value
val simpleString = ReferenceOr.Value(Schema[String].toOpenAPISchema)
Security Schemes
Define authentication methods in components.securitySchemes:
import zio.blocks.openapi._
import zio.blocks.docs._
import zio.blocks.chunk._
import zio.blocks.schema._
val apiKeyScheme = SecurityScheme.APIKey(
name = "X-API-Key",
in = APIKeyLocation.Header,
description = Some(md"API key for authentication")
)
val oauthScheme = SecurityScheme.OAuth2(
flows = OAuthFlows(
authorizationCode = Some(OAuthFlow(
authorizationUrl = Some("https://example.com/oauth/authorize"),
tokenUrl = Some("https://example.com/oauth/token"),
scopes = ChunkMap("read" -> "Read access", "write" -> "Write access")
))
),
description = Some(md"OAuth 2.0 authorization")
)
val components = Components(
securitySchemes = ChunkMap(
"api_key" -> ReferenceOr.Value(apiKeyScheme),
"oauth2" -> ReferenceOr.Value(oauthScheme)
)
)
Path Parameters vs. Query Parameters
Distinguish parameter locations using ParameterLocation:
import zio.blocks.openapi._
import zio.blocks.docs._
import zio.blocks.chunk._
import zio.blocks.schema._
val pathParam = Parameter(
name = "id",
in = ParameterLocation.Path,
required = true,
schema = Some(ReferenceOr.Value(Schema[Int].toOpenAPISchema))
)
val queryParam = Parameter(
name = "limit",
in = ParameterLocation.Query,
required = false,
schema = Some(ReferenceOr.Value(Schema[Int].toOpenAPISchema))
)
val headerParam = Parameter(
name = "X-Custom-Header",
in = ParameterLocation.Header,
required = false,
schema = Some(ReferenceOr.Value(Schema[String].toOpenAPISchema))
)
Integration Points
The OpenAPI module integrates tightly with other ZIO Blocks components:
- Schema Integration: All OpenAPI types have
Schema.derivedinstances, enabling round-trip serialization viaDynamicValue. UseSchema[A].toOpenAPISchemato convert any schema to an OpenAPI component. - Markdown Support: Description fields use the in-house
Doctype, which supports CommonMark rendering. This ensures markdown descriptions round-trip correctly. - JSON AST: All codecs operate on the
JsonAST fromzio-blocks-schema, not external JSON libraries. To render as YAML, pipe theJsonthroughzio-blocks-schema-yamlseparately.
OpenAPI
OpenAPI is the root document object representing a complete OpenAPI 3.1 specification.
Definition
Every OpenAPI document requires:
openapi: Version string (typically"3.1.0")info: Metadata about the API (Info)
Optional top-level fields include:
servers: Server definitions for the API (Chunk[Server])paths: Map of endpoint paths to operations (Paths)components: Reusable schemas, responses, parameters, and other components (Components)security: Security requirements applied to the API (Chunk[SecurityRequirement])
Response definitions are modeled per Operation, not as a top-level field on OpenAPI.
Creating an OpenAPI Document
To construct an OpenAPI document:
import zio.blocks.openapi._
import zio.blocks.docs._
import zio.blocks.chunk._
import zio.blocks.schema._
val minimalApi = OpenAPI(
openapi = "3.1.0",
info = Info(title = "My API", version = "1.0.0")
)
Add paths, operations, and components as shown in the "How They Work Together" section above.
Serialization
Encode an OpenAPI document to Json AST:
import zio.blocks.openapi._
import zio.blocks.openapi.OpenAPICodec._
import zio.blocks.docs._
import zio.blocks.chunk._
import zio.blocks.schema._
import zio.blocks.schema.json._
val myApi = OpenAPI(openapi = "3.1.0", info = Info(title = "My API", version = "1.0.0"))
val encoded: Json = openAPICodec.encodeValue(myApi)
Decode from Json AST back to an OpenAPI instance:
import zio.blocks.openapi._
import zio.blocks.openapi.OpenAPICodec._
import zio.blocks.docs._
import zio.blocks.chunk._
import zio.blocks.schema._
import zio.blocks.schema.json._
val myApi = OpenAPI(openapi = "3.1.0", info = Info(title = "My API", version = "1.0.0"))
val encoded: Json = openAPICodec.encodeValue(myApi)
val decoded: OpenAPI = openAPICodec.decodeValue(encoded)
Info
Info contains metadata about the API: title, version, contact, and license.
Definition
Required fields:
title: API name (e.g.,"User API")version: API version (e.g.,"1.0.0")
Optional fields:
description: Markdown-formatted description (Doc)termsOfService: Terms of service URLcontact: Contact information (Contact)license: License information (License)
Creating Info
import zio.blocks.openapi._
import zio.blocks.docs._
import zio.blocks.chunk._
import zio.blocks.schema._
val info = Info(
title = "Pet Store API",
version = "3.0.0",
description = Some(md"API for managing a pet store"),
contact = Some(Contact(
name = Some("API Support"),
url = Some("https://example.com/support"),
email = Some("support@example.com")
)),
license = Some(License(
name = "Apache 2.0",
identifier = Some("Apache-2.0")
))
)
Paths & PathItem
Paths represents the collection of URL paths and their operations. PathItem groups HTTP methods (GET, POST, PUT, etc.) on a single path.
Definition
Paths is a wrapper case class with two fields:
paths:ChunkMap[String, PathItem], where keys are path strings (e.g.,"/users/{id}")extensions:ChunkMap[String, Json], for OpenAPI specification extensions
PathItem contains optional fields for each HTTP method:
get,post,put,delete,patch,head,options,trace:Operationinstancesparameters: Path-level parameters shared by all methods on this pathservers: Optional server overrides for this path
Creating Path Items
To define a path with multiple operations:
import zio.blocks.openapi._
import zio.blocks.docs._
import zio.blocks.chunk._
import zio.blocks.schema._
val userPaths = Paths(ChunkMap(
"/users" -> PathItem(
get = Some(Operation(
summary = Some(md"List users"),
responses = Responses(ChunkMap(
"200" -> ReferenceOr.Value(Response(
description = md"User list",
content = ChunkMap(
"application/json" -> MediaType(schema = None)
)
))
))
)),
post = Some(Operation(
summary = Some(md"Create user"),
requestBody = Some(ReferenceOr.Value(RequestBody(
description = Some(md"User data"),
content = ChunkMap(
"application/json" -> MediaType(schema = None)
),
required = true
))),
responses = Responses(ChunkMap(
"201" -> ReferenceOr.Value(Response(
description = md"User created",
content = ChunkMap(
"application/json" -> MediaType(schema = None)
)
))
))
))
),
"/users/{id}" -> PathItem(
parameters = Chunk(
ReferenceOr.Value(Parameter(
name = "id",
in = ParameterLocation.Path,
required = true,
schema = Some(ReferenceOr.Value(Schema[String].toOpenAPISchema))
))
),
get = Some(Operation(
summary = Some(md"Get user by ID"),
responses = Responses(ChunkMap(
"200" -> ReferenceOr.Value(Response(
description = md"User found",
content = ChunkMap(
"application/json" -> MediaType(schema = None)
)
)),
"404" -> ReferenceOr.Value(Response(
description = md"User not found",
content = ChunkMap(
"application/json" -> MediaType(schema = None)
)
))
))
))
)
))
Operation
Operation represents a single HTTP operation (GET, POST, etc.) on a path.
Definition
Key fields:
responses: Required. Map of status codes to response definitionsoperationId: Unique operation identifiersummary: Short descriptiondescription: Detailed markdown descriptionparameters: Path, query, header, and cookie parametersrequestBody: Request payload definitiondeprecated: Whether the operation is deprecatedtags: Group operations in documentation (e.g.,"users","products")
Defining an Operation
With summary, description, and parameters:
import zio.blocks.openapi._
import zio.blocks.docs._
import zio.blocks.chunk._
import zio.blocks.schema._
val getUser = Operation(
tags = Chunk("users"),
summary = Some(md"Retrieve user"),
description = Some(md"Fetches a single user by ID"),
operationId = Some("getUserById"),
parameters = Chunk(
ReferenceOr.Value(Parameter(
name = "id",
in = ParameterLocation.Path,
required = true,
schema = Some(ReferenceOr.Value(Schema[String].toOpenAPISchema)),
description = Some(md"User ID")
))
),
responses = Responses(ChunkMap(
"200" -> ReferenceOr.Value(Response(
description = md"User found",
content = ChunkMap(
"application/json" -> MediaType(schema = None)
)
)),
"404" -> ReferenceOr.Value(Response(
description = md"User not found",
content = ChunkMap(
"application/json" -> MediaType(schema = None)
)
))
))
)
Parameter
Parameter represents query, path, header, or cookie parameters in a request.
Definition
Required fields:
name: Parameter name (e.g.,"id","limit")in: Location—Path,Query,Header, orCookie(ParameterLocation)schema: Data type of the parameter (ReferenceOr[SchemaObject])
Optional fields:
description: Markdown descriptionrequired: Whether the parameter is mandatory (default:false)deprecated: Whether the parameter is deprecatedallowEmptyValue: Whether empty string values are allowed
Creating Parameters
Path parameter (required):
import zio.blocks.openapi._
import zio.blocks.docs._
import zio.blocks.chunk._
import zio.blocks.schema._
val idPathParam = Parameter(
name = "id",
in = ParameterLocation.Path,
required = true,
schema = Some(ReferenceOr.Value(Schema[String].toOpenAPISchema)),
description = Some(md"User identifier")
)
Query parameter (optional with default):
import zio.blocks.openapi._
import zio.blocks.docs._
import zio.blocks.chunk._
import zio.blocks.schema._
val limitQueryParam = Parameter(
name = "limit",
in = ParameterLocation.Query,
required = false,
schema = Some(ReferenceOr.Value(Schema[Int].toOpenAPISchema)),
description = Some(md"Maximum number of results (default: 20)")
)
Header parameter:
import zio.blocks.openapi._
import zio.blocks.docs._
import zio.blocks.chunk._
import zio.blocks.schema._
val authHeaderParam = Parameter(
name = "X-API-Key",
in = ParameterLocation.Header,
required = true,
schema = Some(ReferenceOr.Value(Schema[String].toOpenAPISchema)),
description = Some(md"API key for authentication")
)
RequestBody & Response
RequestBody defines the structure of a request payload. Response defines the structure and status of a response.
RequestBody Definition
Key fields:
content: Map of MIME types toMediaTypedefinitionsdescription: Optional markdown descriptionrequired: Whether the request body is mandatory (default:false)
Creating a RequestBody
import zio.blocks.openapi._
import zio.blocks.docs._
import zio.blocks.chunk._
import zio.blocks.schema._
import zio.blocks.schema.json._
case class User(name: String, email: String)
object User { implicit val schema: Schema[User] = Schema.derived }
val createUserBody = RequestBody(
description = Some(md"User data to create"),
content = ChunkMap(
"application/json" -> MediaType(
schema = Some(ReferenceOr.Value(Schema[User].toOpenAPISchema)),
example = Some(Json.Object(Chunk(
"name" -> Json.String("John Doe"),
"email" -> Json.String("john@example.com")
)))
)
),
required = true
)
Response Definition
Key fields:
description: Required. Markdown description of the responsecontent: Map of MIME types toMediaTypedefinitionsheaders: Optional response headerslinks: Optional links to related operations
Creating a Response
import zio.blocks.openapi._
import zio.blocks.docs._
import zio.blocks.chunk._
import zio.blocks.schema._
case class User2(id: Int, name: String, email: String)
object User2 { implicit val schema: Schema[User2] = Schema.derived }
case class ErrorResponse2(code: Int, message: String)
object ErrorResponse2 { implicit val schema: Schema[ErrorResponse2] = Schema.derived }
val successResponse = Response(
description = md"User successfully created",
content = ChunkMap(
"application/json" -> MediaType(
schema = Some(ReferenceOr.Value(Schema[User2].toOpenAPISchema))
)
)
)
val errorResponse = Response(
description = md"Request validation failed",
content = ChunkMap(
"application/json" -> MediaType(
schema = Some(ReferenceOr.Value(Schema[ErrorResponse2].toOpenAPISchema))
)
)
)
Responses
Responses is a map of HTTP status codes to ReferenceOr[Response]:
import zio.blocks.openapi._
import zio.blocks.docs._
import zio.blocks.chunk._
import zio.blocks.schema._
val ok = Response(description = md"Created", content = ChunkMap())
val err = Response(description = md"Bad request", content = ChunkMap())
val responses = Responses(ChunkMap(
"201" -> ReferenceOr.Value(ok),
"400" -> ReferenceOr.Value(err),
"401" -> ReferenceOr.Value(Response(
description = md"Unauthorized",
content = ChunkMap()
)),
"500" -> ReferenceOr.Value(Response(
description = md"Internal server error",
content = ChunkMap()
))
))
MediaType
MediaType specifies the schema and encoding for a particular MIME type in a request or response.