A valid wire-format record
The magic byte, the four-byte schema id, and where the Avro payload starts
00000000010c48656c6c6f
Paste a Kafka message value as base64 or hex and get the first five bytes explained: the magic byte, the 4-byte big-endian schema id, and the exact offset your payload starts at, which is not the same offset for protobuf as for Avro. It runs in your tab, so the message is never sent anywhere, and for the same reason the schema id can be read but never looked up.
Or drop a file anywhere on this panel. Nothing is uploaded: the analysis runs in this tab.
The answer appears here
Paste on the left and press Decode header. Nothing leaves this tab.
Nothing else to flag.
No formatting problems, and nothing the rules object to. Worth remembering what that covers: this reads the file you pasted, not the account or cluster it will be applied to.
No finding matches that filter.
Real input you can load into the tool above. Each one shows a different thing going wrong, because that is what the tool is for.
The magic byte, the four-byte schema id, and where the Avro payload starts
00000000010c48656c6c6f
The single most common cause of a deserializer failing on the first byte
0100000001
These are the ones that fail silently. The config is accepted, nothing raises an error, and the consequence arrives later.
The first five bytes are a magic byte and a schema id, not Avro. Decoding from byte zero fails or, worse, decodes into nonsense.
Instead:Skip five bytes. Every Confluent deserializer does this for you, which is why raw decoding surprises people.
It usually means the data was never written by a Confluent serializer: plain Avro, JSON or Protobuf all start with something else.
Instead:Check what produced the topic before debugging the bytes.
It is a globally unique registry id, not a per-subject version number. Two subjects can be at version 1 with completely different ids.
Instead:Look the id up in the registry to get the subject and version.
Five bytes is a small amount of format to get wrong, and there are four distinct ways to do it. Each one produces a payload that looks corrupted and is not.
Which byte is the magic byte, which four are the schema id, and the offset the payload starts at. The id is read big-endian, because that is what Confluent writes, and the little-endian reading is printed next to it so you can rule byte order out rather than wonder about it.
Confluent's protobuf serialiser writes a message-index array between the id and the payload: a ZigZag varint count, then that many indexes, with a single 0x00 byte as shorthand for the first message type. Avro and JSON Schema have no such array. This page decodes it and gives you the offset for each case, because reading protobuf from offset 5 is precisely the off-by-a-few-bytes garbage decode.
Then it is not this format, and saying which format it is beats pretending to decode it. A leading brace is plain JSON with no framing. A 0x0a is very often a bare protobuf message. Obj followed by 0x01 is a whole Avro container file, which carries its own schema and needs no registry. A 0x03 with eighteen bytes of header is AWS Glue rather than Confluent.
Registries hand out ids from 1, so 0 is never real. When bytes 5 to 8 hold a plausible id instead, that is the signature of Apicurio Registry, whose default handler writes an 8-byte long where Confluent writes a 4-byte int. The symptom is identical to a corrupt buffer and the fix is not.
Id 1234 written little-endian is d2 04 00 00, which Java reads as -771489792 and asks the registry for. Any id past what a registry issues gets checked against its reversed reading, and when that one is plausible, byte order is named as the cause rather than left for you to spot.
Nothing on this page makes a network request, so the id can be read and never looked up. The report says that outright and prints the curl that resolves it against your own registry, including the schemaType field, which is the only reliable way to know whether to strip five bytes or six.
Avro binary and protobuf are both unreadable without the schema that wrote them. Neither carries field names, neither carries types, and both encode integers in a way that depends on what the schema said the field was. There is no reading of the bytes that recovers the record without the schema, only guesses that happen to produce plausible numbers.
The schema lives in your registry, behind the id in bytes 1 to 4. This page makes no network requests, which is the whole promise of the site, so it cannot ask. What it does instead is tell you exactly what to ask for, and warn you when the id in front of you cannot be a real one.
The alternative was a tool that guesses at the payload and prints something confident. On a production message that is worse than printing nothing, so the payload is described here rather than decoded: its length, where it begins for each serialisation format, and a hex dump you can read the string fields out of.
Confluent's serialisers do not write your Avro, protobuf or JSON on its own. They write a magic byte 0x00, a 4-byte big-endian schema id, and then the serialised payload. That is the whole format. It is five bytes, it is not documented in any error message you will see, and it is why a plain deserializer pointed at these bytes fails in a way that looks exactly like data corruption.
One byte of framing, four bytes of schema id, then the payload. The id is a plain 32-bit integer in network byte order, which is big-endian. It identifies a schema inside one registry, and it says nothing about which subject or which version that schema belongs to.
00 00 00 04 d2 06 41 64 61 ...
^ ^~~~~~~~~~~ ^~~~~~~~~~~~~~~
| | payload: Avro, protobuf or JSON, decided by the schema
| schema id 1234, 4-byte big-endian
magic byte, always 0x00 A plain Avro reader starts at byte 0 and reads 0x00 as the first field of your record. Everything after that is off by five bytes, so it reports an unexpected type, a negative string length, or a value that decodes but is nonsense. A JSON parser sees a null byte and gives up on line 1 column 1. Confluent's own deserializer is the one component that says something useful, and even then only if the magic byte is wrong: the message is literally 'Unknown magic byte!', which tells you the framing is missing rather than that the framing exists.
# what people see, in order of how confusing it is
org.apache.avro.AvroRuntimeException: Malformed data. Length is negative
com.fasterxml.jackson.core.JsonParseException: Illegal character ((CTRL-CHAR, code 0))
google.protobuf.message.DecodeError: Error parsing message
org.apache.kafka.common.errors.SerializationException: Unknown magic byte! This is the part that costs an afternoon. A schema id names a whole .proto file, which can declare several message types and nest them, so the id alone does not say which message was serialised. Confluent's protobuf serialiser writes a message-index array after the id to settle it: a ZigZag varint count, then that many ZigZag varint indexes. There is one special case, and it covers most files: a single 0x00 byte means the indexes are [0], the first message type. Avro and JSON Schema have no equivalent, so the offset your payload starts at is 5 for those two and 5 plus the array for protobuf.
Avro and JSON Schema
00 | 00 00 04 d2 | payload ... payload at offset 5
Protobuf, the first message type in the file
00 | 00 00 04 d2 | 00 | payload ... payload at offset 6
^^ the [0] shorthand
Protobuf, the second message type
00 | 00 00 04 d2 | 02 02 | payload ... payload at offset 7
^^ count 1, ZigZag
^^ index 1, ZigZag
Protobuf, a message nested inside the second one
00 | 00 00 04 d2 | 04 02 00 | payload ... payload at offset 8
^^ count 2
^^ ^^ indexes 1 then 0 Two things people assume and neither is true. The id is not the version: version 1 of your subject might be id 47, and version 2 might be id 51, because ids are handed out per schema across the whole registry while versions count within one subject. And the id is only meaningful inside the registry that issued it, so the same four bytes mean a different schema in staging than in production. Copying a payload between environments and expecting it to deserialise is how that gets discovered.
curl -s http://registry:8081/schemas/ids/1234
{"schemaType":"AVRO","schema":"{\"type\":\"record\", ... }"}
curl -s http://registry:8081/schemas/ids/1234/versions
[{"subject":"orders-value","version":3}]
# schemaType is how you learn whether to strip 5 bytes or 6 Five lines in any language, and worth having in a debugging script rather than reaching for a consumer with a registry client configured. Two details to keep right: the integer is big-endian, and Java reads it signed, so a payload with a corrupted top byte asks the registry for a negative id and gets a 404 rather than the large positive number you might expect.
# Python
import struct
magic, schema_id = struct.unpack(">bI", value[:5])
assert magic == 0
payload = value[5:] # Avro and JSON Schema
// Java, which is what the Confluent deserializer does
ByteBuffer buffer = ByteBuffer.wrap(value);
if (buffer.get() != 0x0) throw new SerializationException("Unknown magic byte!");
int schemaId = buffer.getInt(); // signed, big-endian
# shell, for one message off a topic
kcat -C -t orders -c 1 -e -f '%s' | xxd | head -1 If a producer registers schemas for both, the key carries its own 5-byte header and its own schema id, usually pointing at a different subject: orders-key rather than orders-value under the default TopicNameStrategy. A key that is a plain string, which is common, carries no framing at all. So a key and a value off the same topic can legitimately disagree about whether the first byte is 0x00, and that is not a bug.
The magic byte 0x00 is Confluent's. Apicurio Registry's default id handler writes an 8-byte long where Confluent writes a 4-byte int, behind the same 0x00, which means a Confluent-shaped read of an Apicurio payload reports schema id 0 with the real id sitting in the next four bytes. AWS Glue Schema Registry does not use 0x00 at all: it writes a header version byte 0x03, a compression byte, then a 16-byte schema version UUID, so eighteen bytes of framing and an identifier that is not a number. Both produce exactly the same symptom as a Confluent mismatch and neither has the same fix.
Confluent 00 | 00 00 04 d2 | payload
Apicurio 00 | 00 00 00 00 00 00 00 37 | payload
AWS Glue 03 | 00 | 8f 4c 1d 2e ... (16 bytes) | payload
^^ compression: 00 none, 05 zlib