Skip to content

toResult()/mapToResult() rely on Ktor's implicit body<T>() converter: String responses keep their JSON quotes #110

Description

@mfabisiak

Description

toResult() (ApiClientBase.kt) resolves the success body via plain body<T>():

public suspend inline fun <reified E, reified T> HttpResponse.toResult(): HttpResult<E, T> =
    mapToResult { body() }

For reified T = String, Ktor's client has a built-in default converter for
String/ByteArray/Unit/HttpStatusCode/ByteReadChannel that decodes the byte stream using
the response charset — bypassing ContentNegotiation entirely, regardless of
Content-Type: application/json. When an endpoint's OpenAPI response schema is a bare
{"type": "string"} and the server sends a JSON string literal (e.g. "abc-123"), the surrounding
quotes land in the decoded value verbatim instead of being stripped by JSON decoding.

Root cause

Generic body<T>() in mapToResult/toResult relies on Ktor's implicit response-transformation
pipeline, which silently diverges for String vs. every other generated model type.

Proposed fix

Stop relying on Ktor's implicit default converter for String. Decode every success body
explicitly and uniformly through the same configured Json instance (the one already carrying
serializersModule, currently only wired into ContentNegotiation inside createHttpClient)
instead of the ambient body<T>():

protected suspend inline fun <reified E, reified T> HttpResponse.toResult(): HttpResult<E, T> =
    mapToResult { json.decodeFromString(bodyAsText()) }

This requires promoting toResult/mapToResult from top-level extension functions to members of
ApiClientBase so they can reach the shared json property, and removing the reliance on Ktor's
default converters altogether — not just papering over String with an if.

Acceptance criteria

  • A String-typed success response containing a JSON-quoted value (e.g. "abc-123") decodes
    to the unquoted value (abc-123), covered by a MockEngine test
  • Non-String success bodies (data classes, List<T>, polymorphic oneOf types relying on
    serializersModule) continue to deserialize identically to today — no regression from
    switching off the implicit body<T>() path
  • Covered by regression tests in ApiClientBaseGeneratorTest and/or
    JustworksPluginFunctionalTest

Discovered in

Split out from #108 — originally reported together as two independent bugs sharing the same
quote-trim anti-pattern. Reproduced against justworks v0.2.5 / commit 088db52.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions