Stringers
Every path parameter and query parameter — and every non-file multipart part — is a String on the wire.
A Stringer<T> is the two-way conversion between a type and that String.
This page covers the built-in ones, how to write your own, and how to register it.
public interface Stringer<T> {
public fun encode(value: T): String
public fun decode(encoded: String): Either<T, Stringer.Failure>
public val contentType: ContentType get() = ContentType.Text.Plain
}
Escaping is not a Stringer's job.
The runtime percent-encodes every path segment and query value on its way out, and decodes it on the way back in, so encode/decode always work in unescaped text.
A multipart part is not percent-encoded at all: it carries its Stringer's contentType as its own Content-Type header, which is the only place that property is used.
Built-ins
| Type | Encoding |
|---|---|
|
|
|
Decimal, via |
|
Decimal, via |
|
Passthrough — sent and received unchanged. |
They sit at the lowest priority of the resolution order, so a project may register its own Stringer to override one of them, exactly as for any other type.
Declaring one
Any other type needs a Stringer you write.
It must be an object:
object UuidStringer : Stringer<Uuid> {
override fun encode(value: Uuid): String = value.toString()
override fun decode(encoded: String): Either<Uuid, Stringer.Failure> =
Uuid.parseOrNull(encoded)?.let { Either.value(it) } ?: Either.error(Stringer.Failure("Invalid UUID"))
}
Registering one
There are five ways, all equivalent, chosen by how widely the Stringer should apply.
@Endpoint.UseStringer is repeatable, so any of the last four can register as many types as you need from a single place.
@Endpoint.Resource interface Article {
@Endpoint.Stringed(with = UuidStringer::class)
val articleId: Uuid
}
@Endpoint.Service
interface ArticlesService : EndpointService {
@Endpoint.UseStringer(Instant::class, EpochMillisStringer::class)
suspend fun Articles.get(@Endpoint.Query since: Instant? = null): Either<List<ArticleSummary>?, NoFail>
}
@Endpoint.UseStringer(type = Uuid::class, with = UuidStringer::class)
@Endpoint.Resource interface Article {
val articleId: Uuid
}
@Endpoint.UseStringer(type = Uuid::class, with = UuidStringer::class)
@Endpoint.Service interface ServiceBase : EndpointService
@Endpoint.Service interface ArticlesService : ServiceBase { /* ... */ }
@Retention(AnnotationRetention.BINARY) (1)
@Target(AnnotationTarget.CLASS)
@Endpoint.UseStringer(Uuid::class, UuidStringer::class) (2)
@Endpoint.UseStringer(Instant::class, InstantStringer::class)
annotation class UseSharedStringers
@UseSharedStringers
@Endpoint.Resource interface Article {
val articleId: Uuid
}
@UseSharedStringers
@Endpoint.Service interface ArticlesService : EndpointService {
suspend fun Article.get(@Endpoint.Query since: Instant? = null): Either<ArticleContent, NoFail>
}
| 1 | BINARY or RUNTIME: a SOURCE annotation cannot be resolved from a dependent module. |
| 2 | Stack one per type, as @Endpoint.UseStringer repeats. |
InstantStringer here is declared the same way as UuidStringer above.
One annotation now carries every Stringer a project shares between its resources and services.
Resolution order
The nearest declaration wins:
-
@Endpoint.Stringedon the property, query parameter or multipart part payload itself. -
Otherwise, the endpoint function, for a query parameter or a multipart part payload.
-
Otherwise, the part type, for a multipart part payload.
-
Otherwise, the declaring interface and each interface outward — nearest first — checked for an
@Endpoint.UseStringerreachable from its own annotation tree. -
Otherwise, the built-in
StringerforBoolean,Int,LongorString.
Scope distance always dominates annotation depth: a registration on a nearer scope wins over one on a farther scope, however many carrier annotations either is reached through.
An endpoint function’s registration reaches that endpoint’s query parameters and multipart parts, but never its receiver resource’s path parameters. A resource is analysed on its own, and the factory generated for it has one encoding whichever endpoint addresses it.