Generating servers
The same document generates a server-stub module. The document stays the source of truth: generate the client and the server from one specification and implement one handler function per operation.
This runtime supports generated-code contracts 3 through 4. Existing contract-3 servers keep their generated behavior; regenerate them to use OpenAPI.Reply. Contract-4 modules require OpenAPI.jl 1.2 or later. See the runtime contract.
using OpenAPI, HTTP
OpenAPI.server(
"https://example.com/openapi.yaml";
framework = :HTTP,
name = "ExampleServer",
path = "ExampleServer.jl",
)framework = :HTTP (the default, available when HTTP.jl is loaded) targets HTTP.Router. Server framework packages add their own emitters through the OpenAPI.server_source extension seam. An extension must assemble its generated module through OpenAPI.server_module_source; this keeps the runtime data, pasted server code, and generated-code contract guard together. OpenAPI.serverplan is the staged sibling of OpenAPI.plan and rejects documents whose requests cannot be decoded faithfully (for example multipart/mixed request bodies, or two exploded object query parameters whose wire names cannot be told apart).
The generated header
The generated module header lists every handler signature the implementation must define. For a small document:
using OpenAPI, HTTP
document = """
openapi: 3.1.0
info: {title: Widgets, version: 1.0.0}
paths:
/widgets/{id}:
get:
operationId: getWidget
parameters:
- {name: id, in: path, required: true, schema: {type: integer, format: int64}}
- {name: verbose, in: query, schema: {type: boolean}}
responses:
"200":
description: one widget
content:
application/json:
schema:
type: object
required: [id, name]
properties:
id: {type: integer, format: int64}
name: {type: string}
delete:
operationId: deleteWidget
parameters:
- {name: id, in: path, required: true, schema: {type: integer, format: int64}}
responses:
"204": {description: deleted}
"""
source_code = OpenAPI.server(document; framework = :HTTP, name = "WidgetsServer")
header = Iterators.takewhile(!startswith("module"), eachsplit(source_code, '\n'))
println(join(header, '\n'))# Generated by OpenAPI.jl v1.2.0 from "Widgets" version "1.0.0". Do not edit.
# Implement these handler functions in a module (or any value
# supporting `getfield`) and mount them with `register!(router, impl)`:
# deletewidget(request, id::Int64)
# getwidget(request, id::Int64; verbose::Union{Absent,Bool} = ABSENT)
# Plain results use the first documented success response. Return
# OpenAPI.Reply(status, body) to select and validate a different response,
# or a framework response for custom, unvalidated output.Implementing handlers
Handler functions receive the raw request first, then typed path parameters in template order, then a required body; optional parameters arrive as keyword arguments only when the request supplied them.
include("ExampleServer.jl")
module Handlers
using HTTP
# GET /widgets/{id} -> get_widget(request, id::Int64; verbose = ABSENT)
function get_widget(request, id; verbose = false)
return lookup_widget(id; verbose) # encoded, validated, 200
end
# This operation documents 204, so nothing becomes an empty 204 response.
delete_widget(request, id) = nothing
# Return an HTTP.Response directly for custom behavior.
create_widget(request, body) = HTTP.Response(409, "already exists")
end
router = HTTP.Router()
ExampleServer.register!(router, Handlers; path_prefix = "/v1")
server = HTTP.serve!(router, "127.0.0.1", 8080)register!(router, impl; path_prefix, middleware) mounts every documented operation and fails eagerly, listing the expected signatures, when impl is missing any handler. middleware wraps each operation handler (middleware(handler) -> handler). register is kept as an alias, and the handler contract — implementation module second, typed positional parameters, typed-value-or-HTTP.Response returns — matches the shape OpenAPI.jl 0.2.x users generated with -g julia-server.
Developing handlers with Revise
Revise.jl updates handler methods, but a running server keeps calling the definitions that existed when it started. HTTP.jl 2.x does not call handlers through Base.invokelatest, so a server task does not see methods defined after it began. Restart the server, or wrap each handler with Base.invokelatest through middleware while developing:
using Revise, HTTP
includet("Handlers.jl") # defines module Handlers
include("ExampleServer.jl")
router = HTTP.Router()
ExampleServer.register!(router, Handlers;
middleware = handler -> (request -> Base.invokelatest(handler, request)))
server = HTTP.serve!(router, "127.0.0.1", 8080)Once Revise applies an edit to Handlers.jl (before your next REPL command, or when you call Revise.revise()), the next request uses the new definition without a restart. Generated servers do not do this by default because Base.invokelatest cannot be compiled with Julia's --trim option, and generated servers stay trim-compatible. Leave this middleware out of trimmed builds.
Choosing a response status
A plain return value uses the first documented success response. When an operation documents several outcomes, return OpenAPI.Reply to choose the status explicitly while retaining body validation and encoding:
using OpenAPI
function submit(request, body)
if needs_processing(body)
return OpenAPI.Reply(202, (; ticket = "queued"))
end
return (; id = body.id) # this operation documents its first success as 200
end
function get_item(request, id)
item = lookup_item(id)
item === nothing && return OpenAPI.Reply(404, (; message = "no such item"))
return item
endThe body can be a generated model, named tuple, dictionary or another value the documented media type supports. It must satisfy the schema selected by the status: an exact code takes precedence over a range such as 4XX, which takes precedence over default. The explicit status is sent unchanged; it is never inferred from the body's Julia type. Two statuses may use the same body type.
Reply accepts final HTTP statuses from 200 through 599. Informational 1xx responses are not handler results. A status with no exact, range or default entry fails with an error naming the operation and status; the HTTP extension reports this as a server error. Schema failures also produce a server error. OpenAPI.Reply(204, nothing) sends an empty body when 204 documents no content; nothing becomes JSON null only when the selected JSON schema accepts it.
For custom headers or output that deliberately bypasses generated validation, return an HTTP.Response directly. Reply does not add header or media-type overrides.
Request decoding and response encoding
Request decoding mirrors client encoding: parameter styles (simple, label, matrix, form, spaceDelimited, pipeDelimited, deepObject), header and cookie parameters, JSON, application/x-www-form-urlencoded, and multipart/form-data request bodies, with request-direction schema validation before handlers run. Decoding failures produce structured JSON 400 (or 415 for undocumented media types) responses without invoking the handler. Response values are validated against the output-direction schema and encoded from the first documented success response, or the status selected by OpenAPI.Reply. Returning nothing follows that response: it emits an empty body when the response has no content, or JSON null when the selected JSON schema accepts null. A full HTTP.Response bypasses generated status, header, and body validation. The handler owns that validation.
One client capability has no server counterpart yet: a path parameter declared allowReserved: true. Generated clients send such a value with its reserved characters intact, so a slash-delimited value spans several path segments, but generated servers register the path template as written and HTTP.Router matches {name} against a single segment. Those requests reach the router as 404s rather than the handler.
deepObject bracket paths
OAS 3.x defines deepObject only for objects whose property values are scalars (filter[role]=admin). OpenAPI.jl extends the style with the bracket convention that qs, Rack, and PHP produce, so deepObject parameters may declare array schemas and nested object or array values. Nested objects nest brackets, arrays use zero-based bracket indices, and servers also accept name[]=value for appended items:
filters[0][field]=severity&filters[0][values][0]=error&filters[0][values][1]=warning
sorts[0]=-created_at&sorts[1]=%2BnameGenerated clients emit this form and generated servers decode it, in both strict and permissive mode. Bracket keys may arrive percent-encoded. The server rebuilds the value in the shape the parameter schema declares: integer keys become array indices only where the schema expects an array, so an additionalProperties object keeps "0" as a string key, and leaf values stay strings where the schema says string rather than being read as numbers. Schemas reached through $ref, allOf, oneOf, and anyOf are followed. The result is then validated against the schema as for every other parameter. Only scalar deepObject schemas remain a permissive-mode compatibility case.
An operation that documents no success response at all (only error entries) produces a missing_success_response planning warning; its handler's nothing return is answered with an empty 200, which the OpenAPI specification permits — response documentation is explicitly non-exhaustive.
Security and unsupported encodings
Generated server stubs do not authenticate or authorize requests. Apply a middleware that enforces the operation's security policy before it calls the handler. serverplan rejects request-body Encoding Objects that the generated HTTP server cannot recover faithfully. This includes nested encodings, custom multipart part headers, and explicit body encoding style modifiers.