Skip to content

Annotation reference

AnnotationTargetContract
@CommandClassMarks a model-bound command with an instance handle method.
@ReadModelClassMarks a model whose static or companion methods are queries.
@ExportedTypeClassMakes a type an artifact root when no command or query references it. See Exported types.
@CommandKeyProperty, field, value parameter, getterSelects the command identity. Required for plain Chronicle event responses.
@FromServicesValue parameterResolves a query parameter from the host service container instead of the request. Command handler dependencies are service-resolved by their generated signature.
@TreatWarningsAsErrorsClass, functionMakes information and warning validation feedback blocking by default.

Generated command, model, read-model, and interface sequence properties require nonnullable elements. KSP reports ARCKSP0300 for nullable entries, including Java record List<@Nullable T> type uses; nullable outer List<T>?, Collection<T>?, and Array<T>? remain supported. This is an earlier source rejection of an already unsupported manifest shape, not a source-compatible change. See sequence properties.

Public declared Kotlin body properties participate alongside constructor properties: body @CommandKey, field/getter constraints, @Valid, and summaries are retained. Multiple constructor/body keys fail with ARCKSP0106. Member @JsonIgnore excludes state; computed or explicitly read-only Kotlin command input and unrepresentable body Jackson names/access fail with ARCKSP0300. Separate output-only computed models remain supported. See command body state for ordering, Jackson mutability, and inheritance boundaries.

Code generation normally reaches model types through @Command and @ReadModel. A module that contributes only identity detail types or shared data-transfer types has no such root, so it emits no manifest and no generated client. @ExportedType makes the annotated type a root: it and every type reachable from it are collected, and the compilation emits an artifact module and manifest even with no commands and no queries.

Apply it to a public top-level concrete class, enum class, interface, or concept. Unsupported targets — abstract classes, generic definitions, nonpublic types, and nested types — fail with ARCKSP0306. Abstract bases are reached through their concrete derived types instead.

@ExportedType
public data class SampleIdentityDetails(public val source: String)

This is a deliberate divergence from Arc .NET, which discovers identity detail types by runtime reflection over the classpath and exports whole assemblies with a --library-mode flag. KSP runs at compile time, cannot see into function bodies, and cannot enumerate dependency types, so Arc.Kotlin requires the marker rather than inferring roots and silently missing the ones it cannot observe.

KSP uses Kotlin visibility, not the presence of a public keyword. Implicit-public command/read-model classes, handle, provide, companion queries, constructor/body properties, and public interface overrides are supported. A public property with a private setter remains included; private, internal, and protected Kotlin state remains excluded. Generated framework declarations retain explicit public keywords.

Artifacts must still be top-level public classes. Nonpublic command/read-model classes fail with ARCKSP0101/ARCKSP0200; nonpublic handle/provide/query methods fail with ARCKSP0102/ARCKSP0103/ARCKSP0201. An actual query in a private or internal companion also fails with ARCKSP0201; unrelated private companion helpers do not become queries. Java package visibility is not public, and existing Java record/property handling is unchanged. This does not add a constructor-visibility requirement or broaden supported nested model shapes.

Previously omitted implicit-public properties now contribute fields, keys, validation, and reachable types to descriptors, manifests, and clients. Existing input-shape diagnostics also apply to them, including ARCKSP0300 for computed command input. Implicit-public command-like methods and state now participate in the missing-@Command warning (ARCKSP0100), and implicit external handlers accepting an @Command are rejected with ARCKSP0102. Newly accepted provide methods retain the unused-value warning (ARCKSP0107); newly discovered duplicate keys fail with ARCKSP0106. Review regenerated clients and compiler diagnostics when upgrading; this is a source-acceptance correction with no ABI or manifest-format change.

Shared fluent validators are discovered from public final direct FluentModelValidator<T> declarations, not a replacement annotation. They add validated explicit rules to the existing metadata graph and generated runtime registration. ARCKSP0308 rejects unsupported constructor bodies, ARCKSP0309 rejects unrepresentable rules, and ARCKSP0310 rejects missing/conflicting compiler metadata. New shared creditCard() is rejected; the legacy annotation boundary below is unchanged.

AnnotationTargetContract
@IgnoreValidationKotlin property/field/getter; Java instance field/bean getter/record componentCuts one logical validation edge before access while keeping serialization and binding. See member opt-out and Jakarta factory integration, including ARCKSP0311 boundaries.
@PhoneField, property getter, value parameter, annotation classAccepts empty values or strings containing only ASCII digits, JavaScript whitespace, parentheses, plus, and hyphen; use a presence constraint when empty is invalid.
@UrlField, property getter, value parameter, annotation classAccepts empty values or strings beginning with http:// or https:// and a nonempty value after the scheme; use a presence constraint when empty is invalid.
@CreditCardField, property getter, value parameter, annotation classAccepts empty values or Luhn-valid ASCII card numbers containing optional spaces or hyphens; use a presence constraint when empty is invalid.

@Phone, @Url, and @CreditCard are Jakarta constraints, work from Kotlin and Java, and remain in generated validation metadata. @Phone and @Url also emit matching TypeScript runtime rules. @CreditCard is server-only because the pinned @cratis/arc client runtime has no compatible credit-card rule; emitting the current .NET extractor shape would make the proxy uncompilable. Hibernate Validator @URL follows the same client mapping as @Url when present at compile time, while @CreditCardNumber has the same server-only boundary as @CreditCard. null is valid, so nullability remains the responsibility of @NotNull.

AnnotationTargetValues and behavior
@Path(value)Class, functionOverrides query path metadata. Explicit query paths are preserved verbatim.
@QueryHttpMethod(value)Read-model class, functionProxy preference: AUTO (default), GET, or QUERY. A class value defaults every query; a method value overrides it.
@QueryTransport(value)FunctionREQUEST_RESPONSE (default) or OBSERVABLE. KSP infers OBSERVABLE for Kotlin Flow and JDK Flow.Publisher; Spring hosts observable HTTP snapshots, direct SSE/WebSocket, and multiplexed hubs.
AnnotationTargetValues and behavior
@CommandEventSourceType(value)Command classDefault event-source type for events returned by the command.
@CommandEventStreamType(value)Command classDefault event-stream type for returned events.
@CommandEventStreamId(value)Command classDefault event-stream identifier; when absent Chronicle uses the event-source identifier.
@CommandEventSubject(value)Command classDefault compliance subject; when absent Chronicle uses the event-source identifier.

Each annotation is optional. A present value must be nonblank and contain no control characters; KSP reports ARCKSP0110 otherwise. Generated CommandEventMetadata retains absence as null, so Chronicle remains responsible for its own Default and event-source-ID fallbacks rather than Arc serializing those literals.

These are static command defaults. A command can implement CommandEventStreamIdProvider or CommandEventSubjectProvider when those values depend on its instance; generated code rejects a static annotation combined with the matching provider under ARCKSP0110, so one slot never has two sources. An explicit EventForEventSourceId value keeps its own stream/source/subject metadata, while a missing wrapper value inherits the composed command declaration. This deliberately preserves the JVM’s existing explicit-event precedence.

AnnotationTargetValues and behavior
@AllowAnonymousClass, functionAllows unauthenticated access.
@AuthorizeClass, functionOptional policy, roles, and schemes arrays. Requires an authenticated caller.
@Roles(vararg value)Class, functionRepeatable declaration requiring at least one named role.
@RolesContainer(value)Class, functionJVM container generated for repeated @Roles; application code normally does not use it directly.

An operation — a command handle function or a read-model query function — that declares any @Authorize or @Roles replaces its class’s declaration completely. Its policy, roles, and schemes are the only ones evaluated, and the class’s are discarded. The class declaration applies only to operations that declare none. An operation can therefore only narrow access, never widen it: on a class requiring admin, a @Roles("auditor") operation admits auditors and rejects admins. Repeating @Roles on the same target still combines those roles, and a caller satisfies a role list by holding any one of its roles.

@AllowAnonymous cannot be combined with @Authorize or @Roles, on the same target or across a class and its operation. KSP reports ARCKSP0108 and stops generation rather than resolving the combination. This is stricter than Arc .NET, which lets a method-level attribute override the class in that case; declare the artifact so that one level owns the decision.

AnnotationTargetContract
@DerivedType(id)ClassAdds _derivedTypeId and registers a stable identifier for polymorphic Arc JSON. The identifier must be nonblank and unique for its base type. Code generation records the base-to-derivative mappings, and a host registers them so the identifier resolves when reading. See Polymorphic reads.
@FlagsClassMarks an enum as a bit field. Generated TypeScript gains an all<Name> constant combining every nonzero member. Nothing about JVM serialization changes.
@ArcEnumValue(value)FieldDeclares an enum member’s integer wire value where KSP cannot prove it from a single integer-literal constructor argument.

The annotation alone does not make a value readable again. Resolving an identifier back to a class is the job of DerivedTypeRegistry, and Arc never scans the classpath to fill it, so a base type’s concrete types must be registered before it is first deserialized.

Serialization refuses a value whose base type is registered while the value’s own type is not, and names both types. Such a value would otherwise be written with an identifier nothing can resolve and could never be read back. A type whose base type has no registrations at all is still written with its identifier, so a model that only travels to a client keeps working without a registry.

Arc writes an enum as an integer: the result of value() when the enum implements ArcEnum, and the ordinal otherwise. Reading accepts that integer or the member name matched case-insensitively, and rejects anything else.

Declare polymorphic properties as an interface or abstract base, not an ordinary concrete base class. KSP reports ARCKSP0305 when a collected command, read-model, interface, or reachable DTO property declares a concrete class with a distinct, nonabstract @DerivedType descendant visible in the current compilation. The check also applies to nullable properties and the element type of supported collections and arrays. Annotating the concrete base itself with @DerivedType does not exempt it when it has descendants.

A concrete leaf remains legal, as does ordinary inheritance without annotated descendants. Visiting a superclass to collect inheritance metadata is not itself a prohibited property use. Existing map restrictions are unchanged.

This authoring restriction prevents a concrete base instance from writing JSON without _derivedTypeId that a registered polymorphic base requires on read. It is a property-use check, not a guarantee for every runtime registry configuration: changing a declaration to an interface or abstract base does not validate arbitrary multilevel registrations. Root read-model types are not rejected solely for being concrete polymorphic bases. KSP checks source descendants, including those generated in later processing rounds and source descendants of dependency bases; binary-only descendants or manually registered types not visible to KSP are outside this check. Verify those runtime configurations separately.

Source-visible derived leaves generated in later KSP rounds are included in the reachable metadata graph even when they are not commands or read models themselves. Arc refreshes existing interface-property derivative associations with that graph, allowing generated proxies to include the leaf model and its discriminator mapping. An unrelated annotated hierarchy is not included merely because it has @DerivedType annotations.

Populate the DerivedTypeRegistry before reading registered base types with ArcObjectMapper. On Arc’s ordinary discriminator path, runtime dispatch resolves _derivedTypeId only within the declared base type’s registrations; it does not infer self registrations, search other bases, or follow a transitive chain of identifiers. On that path, a non-null value must be an object with a textual, known identifier whose registered target is assignable to that base. Explicit JSON null remains null.

On this ordinary Arc path, manual runtime registrations support multilevel inheritance. For an abstract Root, an annotated concrete Middle extending Root, and an annotated Leaf extending Middle, register each intended pair explicitly: RootMiddle, RootLeaf, and MiddleLeaf. Reading Root with the middle identifier creates exactly Middle, even though Middle is also a registered base. Reading either Root or Middle with the leaf identifier creates Leaf. Reading Middle with its own identifier still fails unless MiddleMiddle was explicitly registered.

Arc consumes only the current object’s discriminator before binding the selected target. Nested properties and collection elements independently validate their identifiers against their own declared bases. Compatible generic type bindings and property-specific Jackson bean configuration are retained during target binding. On this ordinary path, when a registered target declares no type parameters of its own and the requested base is parameterized, Arc also checks the target’s fixed inherited bindings before looking up or invoking its deserializer. A definite contradiction, such as a target extending Base<String> requested as Base<Payload>, fails during reading with JsonMappingException naming the target, requested base, and conflicting binding path rather than returning a value that fails at typed payload access. The check recursively compares available generic arguments, collection elements, map keys and values, reference contents, and array components, projecting compatible subtypes onto the requested raw type before comparing their arguments.

This is read compatibility, not mutable generic invariance: a fixed String binding can be read as String, Object, or CharSequence, and a fixed List<String> as Collection<Object>. Raw or unconstrained requests are allowed; absent or unresolved actual metadata, including Jackson’s indistinguishable Object bindings and erased wildcard constraints, cannot establish a definite contradiction and are not rejected by this check. It does not inspect payload values, validate custom deserializer output or application-defined type policies, or replace Jackson’s existing specialization checks for targets with their own type parameters. JSON null bases remain null; a null payload does not excuse a definite binding contradiction. These are runtime mapper capabilities, not changes to KSP model authoring: the ARCKSP0305 concrete-property restriction above and existing generic model restrictions remain in force.

The Arc wire format is unchanged: annotated objects carry one _derivedTypeId alongside their ordinary properties. Updating readers on the ordinary Arc path retain replacement-read semantics rather than mutating an existing value, and strict Jackson merge configuration rejects those updates.

Existing native Jackson type dispatch, configured through @JsonTypeInfo or default typing, remains Jackson-owned rather than constrained by the Arc registry. The Arc exact-identifier and assignability checks above apply to its ordinary discriminator path, not to native subtype selection. For example, a native name resolver can select a subtype absent from the Arc base’s registrations without _derivedTypeId; adding _derivedTypeId can instead fail as an unknown property on that selected bean. Preserving this existing dispatch is not an Arc round-trip guarantee or a claim that all native combinations are supported, safe, or wire compatible. Separate hardening is tracked in issue #120.

@Flags does not give an enum the ability to carry a combination. A JVM enum constant is one named value, so Read or Write has a constant to deserialize into only when the enum declares one. A generated client can compose such a value — the emitted all<Name> constant exists to be combined with | — and the server answers with the ordinary safe malformedRequest envelope on both a command body and a query argument, without disclosing the enum type. Arc .NET draws the same boundary, because its enum converter gates reads on whether the integer is a defined member.

Two shapes carry a combination:

  • Declare the combination as its own member, which gives it a value to write and a constant to read into.

  • Declare a set of the enum, which writes an array of member wire values and accepts any combination:

    @Flags
    enum class Permission(private val wireValue: Int) : ArcEnum {
    None(0),
    Read(1),
    Write(2);
    override fun value(): Int = wireValue
    }
    data class Grant(val permissions: Set<Permission>)

    A Grant holding Read and Write writes {"permissions":[1,2]} and reads back into the same set.

@DerivedType on its own only affects writing. Resolving _derivedTypeId back to a class is the job of DerivedTypeRegistry, and Arc never scans the classpath to fill it. Code generation records every base-to-derivative mapping it saw on the generated ArcArtifactModule as real class references — the interface a value is declared as, and each base class above it — and the Spring Boot starter registers all of them into its DerivedTypeRegistry bean before Jackson reads anything.

Outside Spring, the same metadata populates a registry directly:

val registry = ConcurrentDerivedTypeRegistry()
ArcArtifactModuleRegistry.registerDerivedTypes(module, registry)
val mapper = ArcObjectMapper.create(registry)

A hierarchy that arrives from a dependency binary Arc’s code generation never processed has no generated mapping. Register one with a DerivedTypeRegistrar bean; registrars run after the generated registrations, in Spring @Order:

@Bean
fun externalShapes() = DerivedTypeRegistrar { registry ->
registry.register(Shape::class.java, ExternalCircle::class.java)
}

An application that declares its own DerivedTypeRegistry bean replaces Arc’s entirely, generated registrations included.

Reading fails closed. An identifier the registry cannot resolve, and a value whose base type has registrations but that carries no _derivedTypeId, are both refused with a mapping error that names the base type; over HTTP that becomes the ordinary safe malformedRequest envelope. Arc .NET refuses an unresolvable identifier the same way, but returns null when the discriminator is absent — an intentional divergence, because a silently absent property is harder to diagnose than a rejected request.