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

  1. Use SpanBytesDecoderDetector.decoderForListMessage(spans) and decode all spans in the list.
  2. Branch on the first byte: '[' or 12 -> list path; otherwise decoderForMessage.
  3. 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

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


AI-assisted analysis of openzipkin/zipkin@878ce2a1fa (2026-08-14). Data as JSON: /api/errors/b611bc1f744751f9. Report an issue: GitHub.