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.

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.1.2 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) -> Nothing
#     getwidget(request, id::Int64; verbose::Union{Absent,Bool} = ABSENT) -> GetwidgetResponse200

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.

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. 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]=%2Bname

Generated 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.