HTTP inbound adapter contract
Status: normative contract for HTTP inbound adapters, including 0.4.0 error policies. The application usecase is the inbound operation contract; this slice adds no duplicate inbound port interface.
Declaration and responsibilities
adapter TourApi inbound http uses CreateTour createTour, ReleaseTour releaseTour {
base "/api/tours";
create {
POST "/";
body NewTour;
execute createTour {
title <- body.title;
capacity <- body.capacity;
}
respond 201 CreatedTour using toCreated;
}
release {
POST "/{id}/release";
path UUID tourId named "id";
execute releaseTour { tourId <- path.tourId; }
respond 204;
}
schema NewTour(String title, int capacity);
schema CreatedTour(UUID id);
mapping toCreated: CreatedTour <- TourId { id <- source; }
}
The example assumes CreateTour(Title, Capacity) returns TourId and
ReleaseTour(TourId) has no result. Dependencies use the shared inferred/explicit
name rules; uses has no parentheses and accepts only required usecases. Each
endpoint has a unique explicit Java-compatible name and invokes exactly one of
those dependencies. It cannot load repositories or execute arbitrary Java statements.
Schemas/mappings can appear before or after endpoints. No custom packages.
The generator produces a constructor-injected Spring RestController in
<namespace>.adapter.inbound.http, a controller-scoped error advice, a <Name>Dtos
holder in its .dto subpackage, and <Name>Mappings in .mapping. The controller
has no transaction annotation; usecase REQUIRED transactions remain authoritative.
Input conversion precedes the usecase invocation. Response mapping occurs after
the usecase returns; its transaction may already have committed. A failed response
does not undo that operation. No automatic retry or idempotency is generated.
Web applications must include Spring MVC, Jackson, vernac-runtime-http and its
JSON runtime dependency at matching versions. Controllers require Spring component
scanning that includes their namespace. No authentication/authorization is implied.
Endpoint clause order
- Explicit endpoint name and opening brace.
- GET, POST, PUT, PATCH or DELETE plus a literal route and semicolon.
- Zero or more path/query/header binding declarations.
- Optional
body Schema;(one required JSON body). execute dependency { parameter <- source [using mapping]; ... }.respond status;orrespond status Schema using mapping;.- Optional
on absent respond status;, then closing brace.
Base/route paths start with /, contain no scheme/host, query, fragment, whitespace,
control characters or literal dot segments. Wildcards and regex path constraints are not supported. Base paths have no placeholders. Route
/ means the base resource itself, without appending a trailing slash. Placeholder
names and bindings must match exactly. Route duplicates are checked across the
project, including placeholders renamed without changing a route’s shape. Runtime
Spring mapping still governs overlaps with handwritten controllers or other modules.
Inputs and ACL
Bindings are typed transport values, not domain objects:
path UUID tourId named "id";
query String? search named "q";
header String tenant named "X-Tenant";
Names may be omitted when the shared type-derived default is useful and unambiguous.
named overrides the external HTTP name. Names are scoped to path/query/header;
headers are compared case-insensitively for duplicate detection. Paths are always
required. Query/header values are required unless optional; optional primitive
bindings must use their boxed type. Collections/domain enums are not wire bindings.
Supported wire types are the approved scalar catalog except EventId. UUID uses the
full canonical hyphenated shape (hex is case-insensitive); boolean accepts only
true/false. Numbers use their Java scalar parsers, currency uses ISO codes, time
values use Java ISO parsers, and Month/DayOfWeek use their Java uppercase names.
ZoneId/ZoneOffset use Java IDs. String is unchanged; char is one UTF-16 code unit.
Values are decoded once by Spring; there is no additional normalization. Invalid
wire input is 400 and the runtime exception does not capture the supplied value.
Sources in execute assignments are path.name, query.name, header.name, body
or body.field.... Targets are actual usecase parameter names. Every required
parameter is assigned once; omitted optional parameters are absent. Unknown fields,
duplicate assignments, incompatible types and optional-to-required assignments fail
compilation. PATCH instructions need an explicit application-value mapping.
Assignments reuse the existing ACL conversion rules: exact types, exact primitive/
boxed counterparts, and required single-value wrapping/unwrapping. UUID to an ID
uses Id.of; String to a String VO uses its validated factory. No text-to-UUID,
unit, enum-code or numeric conversion is inferred by an assignment. Declaring the
path as UUID makes its transport parsing explicit. Complex/nested transformations
use an adapter-local mapping, inline Java or implemented by under the existing
mapping contract.
DTO codecs tolerate additional JSON fields, reject malformed/missing required values, and preserve optional/PATCH semantics. GET bodies are rejected. A body schema is always required; optional whole bodies are not part of this slice.
Results
There is no inferred adapter Result. The result is exactly the type declared by the usecase. Void operations have no response mapping; value-returning operations require one. Success statuses are 2xx. Status 204/205 cannot carry a body.
For returns (TaskId, Title? note), the explicit type <Usecase>.Result (or its fully
qualified Vernac name ending .Result) is available as a mapping source when that
usecase is injected into this adapter. Its components are read through their normal
getters; optional components remain optional. Result is not an ordinary domain type,
DTO declaration, mapping target or something inferred from the Java execute body.
An optional single result requires on absent respond 204;, 404; or 410;.
Absence does not call the response mapper. A present result is mapped normally. A
malformed present value never becomes absence. No Location header is inferred for 201.
Error defaults and extension
Every generated adapter has its own scoped RestControllerAdvice. Defaults do not install a global handler for arbitrary handwritten controllers. Generated advice has Spring order 0; application advice with a higher priority can deliberately override it. Request/response schemas explicitly select application/json.
| Failure | Status / stable code |
|---|---|
| Invalid JSON, missing required binding, invalid typed wire value | 400 / INVALID_REQUEST |
| DomainValidationException during input conversion or usecase execution | 422 / DOMAIN_VALIDATION |
| AggregateNotFoundException | 404 / NOT_FOUND |
| OptimisticLockingFailureException | 409 / OPTIMISTIC_LOCK_CONFLICT |
| PortRejectedException | 422 / the declared rejection enum constant name |
| Other VernacDomainException | 422 / DOMAIN_REJECTED |
| Response-mapping failure, BehaviorContractException, other technical/programming error | 500 / INTERNAL_ERROR |
Bodies contain code and a safe message; exception messages and stack traces are
not automatically published. 5xx failures are logged with their cause. Domain rejection
messages are deliberately generic by default. Custom policies may deliberately supply
public, application-specific text. Authentication/authorization failures handled by
Spring Security filters remain the application’s security configuration responsibility.
Framework failures before a matching controller is selected retain Spring behavior.
At adapter scope, the optional final clause
errors implemented by example.web.ApiErrors; replaces the policy. That class must
provide a public static method:
public static InboundHttpErrors.Reply handle(String endpoint, Exception error)
Use org.vernac.runtime.http.inbound.InboundHttpErrors. Reply contains status (400–599),
code and public message, all required. Return InboundHttpErrors.defaultReply(error)
to retain defaults for unhandled cases. A failing/null custom handler yields a safe 500.
Endpoint names identify failures inside an endpoint; pre-invocation MVC binding errors
use request. Request headers/bodies are not automatically supplied to the policy.
Response-mapping errors are exposed as ResponseMappingFailure with their original
cause, so a default fallback cannot mislabel them as a domain rejection.
No retries, OpenAPI, generated security policy, streaming or asynchronous endpoints are added in this slice. Handwritten Spring adapters remain available when an application’s protocol needs more control.
Verification
Compiler tests reject invalid dependencies, routes, bindings, arguments, result mappings and optional-result policies. Spring MockMvc tests compile the generated controllers/codecs/mappings and execute valid requests, absence, error defaults and an external error policy. They require no database. Existing usecase/persistence tests remain responsible for transaction and database behavior. LSP tests cover local DTO, input and mapping completion/navigation; generated Java origin navigation and inline mapping projections are covered by compiler tooling tests.
Completed pages and cursor failures
Usecase results may contain materialized offset, counted offset and cursor
value-list components. Map them to corresponding page fields in a named local DTO;
using <mapping> applies to entries while metadata is copied unchanged. Query/header
numbers and strings still map into explicitly declared domain inputs. No lazy query
or serialization-time database work is introduced.
InvalidCursorException maps to HTTP 400 with code INVALID_CURSOR; its safe reply asks for a new traversal and does not expose a token, key or internal exception message. Ordinary domain validation stays 422 and optimistic locking stays 409. This applies to blank, malformed or incompatible cursors, not to arbitrary domain exceptions. See the pagination tutorial for a complete board/GET model.
Declarative inbound error policies (0.4.0)
An adapter or endpoint may declare errors { ... } instead of
errors implemented by fully.qualified.JavaClass;. At endpoint scope, the clause
follows respond and optional on absent; the adapter clause follows all endpoints,
schemas and mappings. At least one rule is required. The two policy forms are
mutually exclusive at each scope. Endpoint Java hooks use the same static
handle(String endpoint, Exception error) signature as adapter hooks.
errors {
when rejection ShippingRejection.DESTINATION_NOT_SUPPORTED then respond 422;
when integration remote TIMEOUT then respond 503
"DEPENDENCY_UNAVAILABLE" "A required external service is temporarily unavailable.";
when integration remote INVALID_RESPONSE then respond 502
"DEPENDENCY_INVALID_RESPONSE" "A required external service returned an invalid response.";
when optimistic conflict then respond 409;
otherwise default;
}
| Selector | Matches |
|---|---|
rejection EnumType.CONSTANT |
PortRejectedException carrying that exact enum value; enum type is part of identity |
integration local CATEGORY |
LocalIntegrationException with that IntegrationCategory |
integration remote CATEGORY |
RemoteIntegrationException with that IntegrationCategory |
integration unknown CATEGORY |
PortInvocationException with unassigned responsibility and that category |
invalid request |
Malformed JSON, binding or typed wire value |
invalid cursor |
InvalidCursorException |
validation |
DomainValidationException, excluding InvalidCursorException |
not found |
AggregateNotFoundException |
optimistic conflict |
OptimisticLockingFailureException |
domain rejection |
Any VernacDomainException, including validation, absence and typed rejection |
Rules are tested in declaration order. The first match wins. Endpoint rules run
before adapter rules; when none matches, evaluation continues at adapter scope,
then to the existing safe defaults. An endpoint implemented-by policy is authoritative
and returns the final reply; it does not implicitly fall through to adapter rules. Optional otherwise default; explicitly names
that final fallback and does not prevent inheritance. Duplicate selectors within a
scope are errors. Overlap between different selectors is intentional; put specific
rules before broad domain rejection rules. No automatic specificity reordering.
then respond STATUS; changes only the status of the safe default reply. Alternatively,
then respond STATUS "PUBLIC_CODE" "Public message"; replaces all three values.
Status must be 400–599. Code and message must both be supplied, nonblank, or both
omitted. String escapes follow the existing DSL string-literal rules. Replies retain
the existing JSON { "code": "...", "message": "..." } format; no arbitrary payload,
expression, header, retry or exception-class selector is added.
Enum types/constants resolve through namespace/import rules. Categories are the existing IntegrationCategory constants, not implicitly declared codes. A typed rejection currently requires PortRejectedException and its AdapterInvocation; do not invent an adapter context for a rejection originating directly in a domain model. A general typed domain-rejection mechanism is outside this feature.
HTTP status does not determine failure responsibility. For example, an explicitly remote timeout can be represented by 503 while retaining its original exception, category and adapter invocation for diagnosis. No blanket mapping of integration failures to 503 is added. Public messages must not contain secrets or upstream payloads. Internal exception messages are never copied automatically. 5xx responses retain the existing logging of their cause; no incident identifier is introduced.
Only boundary-owned invocation and recognized input-mapping wrappers are unwrapped
for classification. ResponseMappingFailure is never traversed: a domain exception
raised while producing a response remains a technical response failure. Arbitrary
Java cause chains are not searched. InvalidCursorException is separate from generic
validation but can be intentionally matched by broad domain rejection.
HTTP advice translates errors after usecase invocation has failed. It neither retries
nor changes REQUIRED transaction boundaries or rollback rules. Input failures occur
before the usecase; response failures may occur after commit. MVC errors before an
endpoint invocation use endpoint request, so only adapter rules apply. Application
security filters remain outside these scoped controller policies.
Compiler diagnostics, ordered generated policies, safe runtime classification, Java 21 compilation, MockMvc execution and enum/category editor references are tested.
Server-rendered MVC boundaries (0.5.0 development)
adapter ... inbound mvc shares this contract’s routes, explicit usecase dependencies,
scalar/VO input conversion and error selectors, while returning templates or redirects
instead of JSON DTOs. It adds URL-encoded scalar form bindings, defaults, sensitive
input exclusion and explicit page redisplay. See the MVC contract.
The JSON inbound http form continues to reject MVC-only clauses.
External implementation targets also follow the 0.5.0 shared Java implementation resolution rules. Their mapping/error-handler calling convention is unchanged.