Protobuf to JSON

Decode Protobuf bytes to JSON with or without a .proto file. Hex, Base64 or gRPC frames in; ProtoJSON out with int64 and Timestamp intact. Encode JSON back too.

  • Runs in your browser
  • Your data never leaves your browser
  • Free · No Sign-Up
Binary → JSON decodes Protobuf bytes. JSON → Binary encodes ProtoJSON back to bytes and needs a .proto schema and a message type. Sample JSON prints each field of the selected type once with a placeholder value. In every mode the output updates as you type. Switching to JSON → Binary while its box is empty fills the box with the JSON decoded from the bytes.
The message that the bytes are decoded as, or that the JSON is encoded as. The list holds the types from your .proto files, and the first one that no other type uses as a field is preselected; that is usually the top-level message. No schema (raw decode), in Binary → JSON only, reads the wire format without names.
Auto recognizes hex, Base64, an escaped string or a byte list and names the format in the status line. Text that is valid both as hex and as Base64 is read as hex, with a note; choose Base64 here when that is wrong. Auto removes gRPC framing when the input starts with a frame flag (00, 01, 80 or 81) and the 5-byte headers cover it exactly. Several messages print as a JSON array, gRPC-Web trailers go to the notes, and frames with the compressed flag are gunzipped; other compression is not supported. Yes reports a broken frame as an error. No keeps every byte as message data. lowerCamelCase prints the ProtoJSON name of each field: its json_name option when it has one, otherwise the field name in lowerCamelCase. As in .proto prints the names as the schema writes them. JSON → Binary accepts both forms whatever is selected here. ProtoJSON leaves out a field without presence (a plain proto3 field) when it holds its default value: 0, an empty string, false or the first enum value. Empty repeated and map fields are left out too. Turn this on to print them. A field with presence, such as an optional, oneof or message field, is printed only when the bytes set it. How bytes are shown when they are decoded without a schema. JSON shows a length-delimited value as text when it is readable, as a nested object when it parses as a message, and as base64 otherwise. JSON with other readings also shows each number as the other types of its wire type, such as sint64, sfixed64, float and double. protoc --decode_raw prints what protoc prints, which prefers nested messages. Off: a JSON key that is not a field of the message type stops the encoding, with its line and column and a suggestion when a similar field exists. On: such keys are skipped and listed in the notes, and an enum name that the schema does not define is skipped too.

Paste hex (spaces, line breaks, colons and 0x prefixes are fine), Base64 (standard or URL-safe, with or without padding, or a data: URL), an escaped string such as a Python bytes literal or protoc octal escapes, or a list of byte values from -128 to 255. Decoding runs as you type. Open file, or a file dropped on the box, loads a binary file of up to 2 MB as hex.
ProtoJSON for the selected message type; it is encoded as you type. A key can be the JSON name or the .proto name of a field. Integers can be numbers or strings, enums can be names or numbers, and bytes are Base64. An error gives the line and column.
The .proto source that names the fields. In Binary → JSON it is optional: without it, fields show as numbers. The other two modes need it. Add the files it imports with Add .proto files, or drop them on the box. The google/protobuf well-known types any, duration, empty, field_mask, struct, timestamp and wrappers are built in. A compiled descriptor set cannot be loaded, and custom options that extend descriptor.proto are ignored.
Output
  
Examples, details and FAQ Worked examples, how it compares with other tools, and answers to common questions.

Example: an Order Message

The example loaded on this page is an Order from a small shop schema with an int64 id above 2^53, a google.protobuf.Timestamp and a map<string, string>. Decoded as shop.v1.Order, the 76 bytes give:

{
  "orderId": "9007199254740993",
  "customerName": "Ada Lovelace",
  "items": [
    {
      "sku": "BK-1843",
      "quantity": 2,
      "unitPrice": 19.99
    }
  ],
  "status": "STATUS_PAID",
  "createdAt": "2026-10-01T08:30:00.250Z",
  "labels": {
    "channel": "web"
  }
}

Each line follows the ProtoJSON format: order_id becomes orderId, the 64-bit id is a string, the enum is its name, the Timestamp is an RFC 3339 string in UTC with 3 fraction digits, and the map is a JSON object. Number(9007199254740993) in JavaScript is 9007199254740992, because Number.MAX_SAFE_INTEGER is 2^53 − 1; that is why the format uses strings.

Example: No .proto at Hand

With No schema (raw decode) the same bytes print with field numbers. The protoc —decode_raw view gives exactly what protoc --decode_raw prints:

1: 9007199254740993
2: "Ada Lovelace"
3 {
  1: "BK-1843"
  2: 2
  3: 0x4033fd70a3d70a3d
}
4: 1
5 {
  1: 1790843400
  2: 250000000
}
6 {
  1: "channel"
  2: "web"
}

0x4033fd70a3d70a3d is the 64-bit pattern of the double 19.99, and 5 { 1: 1790843400 2: 250000000 } is the Timestamp’s seconds and nanos. JSON with other readings shows each number as its other possible types (sint64, sfixed64, double, float), so you can tell a signed field from an unsigned one. Note that protoc prefers a nested message over a string: the two bytes hi read as field 13 with value 105. The JSON views show readable text as a string instead.

Example: A gRPC-Web Response

A application/grpc-web-text response is Base64 of the gRPC frames, the last one carrying the trailers. Copied from the browser’s network panel, this body:

AAAAAEwIgYCAgICAgBASDEFkYSBMb3ZlbGFjZRoUCgdCSy0xODQzEAIZPQrXo3D9M0AgASoLCIi0+NUGEIDlmncyDgoHY2hhbm5lbBIDd2VigAAAAB5ncnBjLXN0YXR1czowDQpncnBjLW1lc3NhZ2U6DQo=

decodes to the same order. The tool removes the 5-byte header (flag and big-endian length, as in gRPC over HTTP/2), and lists grpc-status:0 from the trailers frame, whose flag has its top bit set (gRPC-Web), in the notes:

{
  "order_id": "9007199254740993",
  "customer_name": "Ada Lovelace",
  "items": [
    {
      "sku": "BK-1843",
      "quantity": 2,
      "unit_price": 19.99
    }
  ],
  "status": "STATUS_PAID",
  "created_at": "2026-10-01T08:30:00.250Z",
  "labels": {
    "channel": "web"
  }
}

This run used Field names: As in .proto. A streaming response with several messages prints as a JSON array.

Example: JSON Back to Bytes

In JSON → Binary, this ProtoJSON (note the +09:00 offset, which ProtoJSON allows on input):

{"orderId": "42", "customerName": "Ada", "status": "STATUS_SHIPPED", "createdAt": "2026-10-01T17:30:00+09:00"}

encodes to 17 bytes, the same as protoc --encode=shop.v1.Order for that message:

08 2a 12 03 41 64 61 20 02 2a 06 08 88 b4 f8 d5 06

Both orderId and order_id are accepted, and so are enum numbers and int64 as JSON numbers. Mistakes are reported with the line and column: "status": "PAID" is rejected as not a value of shop.v1.Status, followed by the valid names, and a misspelled key such as customer_Name gets a Did you mean “customerName”? hint. Add gRPC frame prefixes the 5-byte header, and the output can be hex, Base64, a Python bytes literal or a C array, or downloaded as a .bin file.

What the Output Follows

The decoder and encoder were checked on 140 random messages against protoc 36.2 and Python protobuf 7.36.2 json_format, including proto2 groups and extensions and edition 2023 features (139 match; the one Python rejects, the tool rejects too):

DataJSON
int64, uint64, sint64, fixed64, sfixed64decimal string
float, doublenumber; "NaN", "Infinity", "-Infinity"; floats print the shortest form (0.1, not 0.10000000149011612)
bytesstandard Base64
enumname; a number the schema does not define stays a number
Timestamp, Duration, FieldMask"2026-10-01T08:30:00Z", "1.5s", "user.displayName"
Struct, Value, ListValue, wrappersplain JSON values
Any"@type" plus the fields, or "value" for a well-known type
default valuesleft out for proto3 fields without presence

How It Differs From Other Online Decoders

Tested on 2026-10-02 with the order above. egohero.com and codertools.net use a .proto: with the Timestamp import, egohero stops with no such Type or Enum ‘google.protobuf.Timestamp’ and codertools lists no message types; with the Timestamp field removed, both print "order_id": "9007199254740992" (one less than the real id) and snake_case names. jsontotable.org in Base64 Binary mode printed "id": 123 and a name that is not in the input; only "originalLength": 76 was right. pawitp’s Protobuf Decoder reads the bytes without a schema, strips the gRPC header and shows sint and double readings, but takes no .proto. None of them sent the input to a server.

Limits

  • Schemas are .proto source. A compiled descriptor set (protoc --descriptor_set_out) or text-format messages cannot be loaded. Bundled imports are any, duration, empty, field_mask, struct, timestamp and wrappers; other imports, such as google/api/annotations.proto, must be added if a type comes from them. Custom options that extend descriptor.proto are ignored, since options do not change the bytes.
  • Unknown fields are not in the JSON. ProtoJSON has no place for them; the notes list each with its field number, wire type and byte offset, and suggest message types that decode the bytes without any.
  • Wrong type, valid output. Bytes carry no type name, so decoding with the wrong message often succeeds with odd values. Check the notes for unknown fields.
  • Nesting depth is 100 and opened files are limited to 2 MB. Compressed gRPC frames are gunzipped; other grpc-encoding values are not supported.
  • proto2 and editions (2023, 2024) are supported for groups, required fields, closed enums, extensions ("[pkg.ext]"), field presence, packed and expanded repeated fields and delimited messages. A proto3 string that is not valid UTF-8 is an error with its byte offset; use bytes for binary data.

Related: inspect captured traffic in the HAR Analyzer & Viewer, convert payloads with Base64 Encode / Decode, and validate JSON with the JSON Schema Validator.

FAQ

Do I need the .proto file to decode Protobuf?

No. Leave the schema box empty and the tool decodes the wire format like protoc --decode_raw: you get field numbers, numbers, strings and nested messages. Field names, enum names, signed and 64-bit types and well-known types such as Timestamp need the .proto, because the bytes do not carry them.

Why are int64 values shown as strings?

The ProtoJSON format writes int64, uint64, sint64, fixed64 and sfixed64 as decimal strings, because JavaScript reads JSON numbers as doubles and loses digits above 2^53. The tool decodes 64-bit fields with BigInt, so 9007199254740993 stays 9007199254740993.

Why is a field missing from the JSON output?

ProtoJSON leaves out proto3 fields that hold their default value (0, empty string, false, first enum value) unless they are optional or in a oneof. Turn on Show fields with default values to print them. Fields whose numbers are not in the schema cannot appear in ProtoJSON at all; the Notes list each one with its byte offset.

How do I decode a gRPC or gRPC-Web payload?

Paste it as captured. With gRPC frame set to Auto, the tool removes the 5-byte header of each message, decodes every message in the body, shows gRPC-Web trailers in the notes and gunzips frames whose compressed flag is set. grpc-web-text bodies are Base64 and can be pasted directly.

Is my data uploaded?

No. Parsing, decoding and encoding run in your browser. The page stores only your option choices, not the schema, the bytes or the JSON.