openzipkin/zipkin · error · IllegalArgumentException
Expected json or thrift object, not list encoding
Error message
Expected json or thrift object, not list encoding
What it means
SpanBytesDecoderDetector.decoderForMessage(byte[]) expects a single-span v1 JSON object or Thrift struct and throws IllegalArgumentException('Expected json or thrift object, not list encoding') when the payload is a list: first byte '[' (JSON array) or 12 (Thrift list-of-span header). Single-message decoding is only for legacy v1 ingestion; lists must go through decoderForListMessage.
Source
Thrown at zipkin/src/main/java/zipkin2/SpanBytesDecoderDetector.java:56
*/
static final byte[] ENDPOINT_FIELD_SUFFIX = {'E', 'n', 'd', 'p', 'o', 'i', 'n', 't', '"'};
/**
* Technically, it is possible to have a v2 span with no endpoints. This should catch the case
* where someone reported a tag without reporting the "localEndpoint".
*
* <p>Note: we don't check for annotations as that exists in both v1 and v2 formats.
*/
static final byte[] TAGS_FIELD = {'"', 't', 'a', 'g', 's', '"'};
/**
* Throws {@link IllegalArgumentException} if the input isn't a v1 json or thrift single-span
* message
*/
public static BytesDecoder<Span> decoderForMessage(byte[] span) {
BytesDecoder<Span> decoder = detectDecoder(ByteBuffer.wrap(span));
if (span[0] == 12 /* List[ThriftSpan] */ || span[0] == '[') {
throw new IllegalArgumentException("Expected json or thrift object, not list encoding");
}
if (decoder == SpanBytesDecoder.JSON_V2 || decoder == SpanBytesDecoder.PROTO3) {
throw new UnsupportedOperationException("v2 formats should only be used with list messages");
}
return decoder;
}
/**
* Throws {@link IllegalArgumentException} if the input isn't a json, proto3 or thrift list
* message.
*/
public static BytesDecoder<Span> decoderForListMessage(byte[] spans) {
return decoderForListMessage(ByteBuffer.wrap(spans));
}
public static BytesDecoder<Span> decoderForListMessage(ByteBuffer spans) {
BytesDecoder<Span> decoder = detectDecoder(spans);
byte first = spans.get(spans.position());View on GitHub (pinned to 878ce2a1fa)
Solutions
- Use SpanBytesDecoderDetector.decoderForListMessage(spans) and decode all spans in the list.
- Branch on the first byte: '[' or 12 -> list path; otherwise decoderForMessage.
- If you must accept both shapes, JSON_V1 and THRIFT decoders have sizeInBytes/list handling — detect first, then decode one-or-many accordingly.
Example fix
// before BytesDecoder<Span> d = SpanBytesDecoderDetector.decoderForMessage(payload); Span span = d.decodeOne(payload); // after BytesDecoder<Span> d = SpanBytesDecoderDetector.decoderForListMessage(payload); List<Span> spans = d.decodeList(payload);
Defensive patterns
Strategy: type-guard
Validate before calling
boolean isListEncoded(byte[] payload) {
byte f = payload[0];
return f == '[' || f == 12 || f == 11;
} Type guard
enum SpanPayloadShape { SINGLE, LIST, UNKNOWN }
SpanPayloadShape shapeOf(byte[] p) {
if (p.length == 0) return SpanPayloadShape.UNKNOWN;
byte f = p[0];
if (f == '[' || f == 12 || f == 11) return SpanPayloadShape.LIST;
if (f == '{') return SpanPayloadShape.SINGLE;
return SpanPayloadShape.UNKNOWN;
} Prevention
- Send and decode reporter payloads as lists (decoderForListMessage) by default.
- Keep the single-message API only for legacy v1 paths.
When it happens
Trigger: Calling decoderForMessage on data produced by zipkin reporters, which send JSON arrays like [{...}] or Thrift List[Span] batches.
Common situations: Writing a custom collector/transport that receives spans from zipkin-reporter (Kafka, SQS, HTTP /spans) but reuses single-span decode logic; migrating legacy v1 single-object ingestion to also accept modern reporter payloads.
Related errors
- v2 formats should only be used with list messages
- Expected json, proto3 or thrift list encoding
- Could not detect the span format
- should be a 1 to 32 character lower-hex string with no pref
- %s reading %s from json
AI-assisted analysis of openzipkin/zipkin@878ce2a1fa (2026-08-14).
Data as JSON: /api/errors/b611bc1f744751f9.
Report an issue: GitHub.