Skip to content

ByteArray (format: byte) properties in JSON models don't round-trip — kotlinx.serialization's default ByteArraySerializer expects an int array, not base64 #114

Description

@mfabisiak

Description

A schema property declared as {type: string, format: byte} is mapped to Kotlin ByteArray
(PrimitiveType.BYTE_ARRAY, see SpecParser.kt), but no serializer is generated or registered
for it. Generated @Serializable data classes with such a property compile fine, but don't
decode real API payloads correctly at runtime.

Root cause

Per OpenAPI 3.0, format: byte means "base64-encoded string" — on the wire it's a JSON string
(e.g. "SGVsbG8="). But kotlinx.serialization's built-in ByteArraySerializer (the one picked up
implicitly for a plain ByteArray property) encodes/decodes it as a JSON array of numbers
([72, 101, ...]) instead. Any real API response with a base64 string in that field throws during
deserialization; conversely, anything we serialize ourselves comes out as a number array a real
API wouldn't accept.

This is the same category of bug fixed for Uuid — a JVM/Kotlin type kotlinx.serialization
doesn't have a wire-compatible built-in serializer for — via a generated UuidSerializer +
@Serializable(with = UuidSerializer::class), opt-in only when the spec actually uses it
(ModelGenerator.kt, generateUuidSerializer()).

Proposed fix

Mirror the existing UuidSerializer pattern: generate a shared Base64ByteArraySerializer
(KSerializer<ByteArray> encoding/decoding via base64), opt-in only when the spec uses
format: byte, and annotate matching model properties with
@Serializable(with = Base64ByteArraySerializer::class).

Once this exists, PR #112's top-level-response handling for a bare format: byte schema under
application/json (currently downgraded to String to avoid the same mismatch — see
ClientGenerator.resolveReturnType) could be revisited to decode straight to a real ByteArray
via the shared serializer instead.

Acceptance criteria

  • A format: byte model property round-trips correctly against a real base64 JSON string
    (covered by a MockEngine/compilation test)
  • Base64ByteArraySerializer is generated only when the spec actually uses format: byte
    (mirroring usesUuid())
  • No change for ByteArray properties/responses under application/octet-stream — those are
    raw bytes already handled correctly via toRawResult()/body<T>()

Discovered in

PR #112 (review by @MattK97) — a format: byte schema under application/json at the top-level
response was found to throw via decodeFromString<ByteArray>() on every call, since
kotlinx.serialization's default ByteArraySerializer doesn't match the base64-string wire format.
PR #112 fixes the top-level-response symptom by downgrading the return type to String; this
issue covers the more general (and pre-existing, not a regression from #112) gap for ByteArray
properties nested inside model classes, which has the identical root cause.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions