apache/iceberg · error · JsonToMapException

Expected json object for record.value after parsing

Error message

Expected json object for record.value after parsing: ${collectRecordDetails(record)}

What it means

JsonToMapTransform requires the parsed JSON to be an object (Jackson ObjectNode). If the value parses successfully but is an array, string, number, or boolean at the root, it throws JsonToMapException('Expected json object for record.value after parsing: ...') because the result must convert to a Map for Iceberg column mapping.

Solutions

  1. Change the producer to emit a top-level JSON object per record
  2. Wrap arrays in an object upstream (e.g. {"records": [...]}) and extract a field via a nested transform
  3. Split array elements into individual records before the sink
  4. Validate topic payloads with kafka-console-consumer to confirm the shape

Example fix

// before (producer)
[{"id":1},{"id":2}]
// after (one object per record)
{"id":1}
{"id":2}
Defensive patterns

Strategy: validation

Validate before calling

boolean isJsonObjectPayload(String s) {
  try { return new ObjectMapper().readTree(s) instanceof ObjectNode; } catch (Exception e) { return false; }
}

Type guard

boolean isObjectNode(JsonNode n) {
  return n instanceof ObjectNode;
}

Try / catch

try {
  return transform.apply(record);
} catch (JsonToMapException e) {
  LOG.error("Root JSON value must be an object", e);
  deadLetterQueue.send(record, e);
  return null;
}

Prevention

When it happens

Trigger: record.value() is valid JSON but its root is a JSON array (e.g. '[1,2,3]'), a quoted string, a number, or null — anything not a '{...}' object.

Common situations: Topics carrying JSON arrays of events instead of single objects; producers emitting scalar JSON payloads; batch-export producers writing newline-batched arrays.

Related errors


AI-assisted analysis of apache/iceberg@86d9c8fc54 (2026-09-12). Data as JSON: /api/errors/39a3bd0f4fc36f09. Report an issue: GitHub.

Appendix: source

Thrown at kafka-connect/kafka-connect-transforms/src/main/java/org/apache/iceberg/connect/transforms/JsonToMapTransform.java:89

  private R process(R record) {
    if (!(record.value() instanceof String)) {
      throw new JsonToMapException("record value is not a string, use StringConverter");
    }

    String json = (String) record.value();
    JsonNode obj;

    try {
      obj = MAPPER.readTree(json);
    } catch (Exception e) {
      throw new JsonToMapException(
          String.format(
              "record.value is not valid json for record.value: %s", collectRecordDetails(record)),
          e);
    }

    if (!(obj instanceof ObjectNode)) {
      throw new JsonToMapException(
          String.format(
              "Expected json object for record.value after parsing: %s",
              collectRecordDetails(record)));
    }

    if (startAtRoot) {
      return singleField(record, (ObjectNode) obj);
    }
    return structRecord(record, (ObjectNode) obj);
  }

  private R singleField(R record, ObjectNode obj) {
    Struct struct =
        new Struct(JSON_MAP_SCHEMA)
            .put(ALL_JSON_SCHEMA_FIELD, JsonToMapUtils.populateMap(obj, Maps.newHashMap()));
    return record.newRecord(
        record.topic(),
        record.kafkaPartition(),

View on GitHub (pinned to 86d9c8fc54)