The NBT File Format, Explained Byte by Byte
NBT (Named Binary Tag) is a compact binary file format for storing tree-structured data. It was originally designed by Markus "Notch" Persson for Minecraft, and it remains the standard way Minecraft Java Edition persists nearly everything that is not a block: world metadata, player state, structure block exports, server lists, scoreboard data and much more. Because the format is publicly documented and simple to implement, dozens of community tools — including the NBTBox online editor — can read and write it.
This guide explains the format the way an implementer needs to understand it: every tag type, the exact byte layout, the compression containers that wrap NBT payloads, and the pitfalls that break naive parsers. No prior knowledge of binary formats is assumed.
1. The mental model: a labeled tree
If you have ever read JSON, you already understand NBT's shape. An NBT document is a single root compound — think of it as a top-level object — which contains named tags. Each named tag has a type, a name, and a payload. A payload can itself be a compound or a list, which is how arbitrary-depth trees emerge:
"" (root compound)
├─ "Data" (compound)
│ ├─ "LevelName" (string) = "My World"
│ ├─ "RandomSeed" (long) = -41728293714
│ ├─ "SpawnX" (int) = 16
│ └─ "GameRules" (compound)
│ └─ "doDaylightCycle" (string) = "true"
└─ "Player" (compound)
└─ "Pos" (list of double) = [-1.5, 64.0, 2.25]
The difference from JSON is that NBT is binary (compact, fast to parse, not human-readable) and strongly typed (every value carries an explicit type tag, and integers come in four widths). There are no "floating" values: except for items inside lists and the root itself, every tag travels with its name.
2. The 13 tag types
Every tag starts with a one-byte type identifier. Types 1–6 are scalars, 7–12 are containers or sequences, and type 0 is a special marker:
| ID | Name | Payload | Size (bytes) |
|---|---|---|---|
| 0 | TAG_End | none — end-of-compound marker | 0 |
| 1 | TAG_Byte | signed 8-bit integer | 1 |
| 2 | TAG_Short | signed 16-bit integer | 2 |
| 3 | TAG_Int | signed 32-bit integer | 4 |
| 4 | TAG_Long | signed 64-bit integer | 8 |
| 5 | TAG_Float | IEEE-754 single precision | 4 |
| 6 | TAG_Double | IEEE-754 double precision | 8 |
| 7 | TAG_ByteArray | length + raw bytes | 4 + n |
| 8 | TAG_String | length + UTF-8 text | 2 + n |
| 9 | TAG_List | element type + count + payloads | 5 + payload |
| 10 | TAG_Compound | named tags + TAG_End | variable |
| 11 | TAG_IntArray | length + 32-bit integers | 4 + 4n |
| 12 | TAG_LongArray | length + 64-bit integers | 4 + 8n |
ByteArray has existed since the earliest versions of the format. IntArray was added during the Java 1.7 development cycle (2013), and LongArray followed in the 1.12 cycle (2017) to serve structure storage. A parser written before those years will choke on type 11 and 12 — one of many reasons to use a maintained tool.
3. Anatomy of a file, byte by byte
An NBT document contains exactly one root tag, which is always a named TAG_Compound. The file therefore begins:
0A 00 00 → TAG_Compound, name length 0, name "" (empty)
That is: byte 0x0A (type 10, compound), then a 16-bit big-endian name length (0x0000 — the root name is conventionally empty), then the compound's payload. If you hex-open a vanilla level.dat after decompression, the first three bytes are always 0A 00 00. A file that begins with anything else in its raw payload is not a Java NBT document.
Here is a tiny complete document — a root compound containing one string and terminating:
0A TAG_Compound (root)
00 00 root name: length 0, ""
08 TAG_String
00 04 name length 4
44 61 74 61 name "Data"
00 05 string length 5
48 65 6C 6C 6F value "Hello"
00 TAG_End — closes the root compound
Sixteen bytes, and you have seen every structural rule in the format: type byte, name, payload, End marker.
4. How numbers are encoded
All NBT integers are signed and stored big-endian (most significant byte first). That is the opposite of the little-endian layout x86 processors use natively, which is why every parser must be explicit about byte order. A TAG_Long storing the decimal value −4,172,829,371,4 is encoded as the eight bytes:
FF FF FF F6 78 5B 1F 7E
Floats and doubles follow IEEE-754 and are also big-endian. One practical consequence for JavaScript developers: a TAG_Long can hold values beyond ±253, the range where JS numbers silently lose precision. Any correct JavaScript implementation must read 64-bit integers as BigInt, not as number. NBTBox does exactly that, which is why seed values survive an edit round trip unchanged.
5. How strings are encoded
A TAG_String payload is an unsigned 16-bit byte length followed by that many bytes of UTF-8 text. Two details matter:
- The length counts bytes, not characters. A name like "坐标规则" is four characters but twelve UTF-8 bytes, so its length field reads 12. Parsers that measure
string.lengthinstead of byte length will corrupt files containing non-ASCII names — and world data uses non-ASCII names more often than you would expect (player names, scoreboard entries, custom game rules). - Historically "modified UTF-8". The format descends from Java's DataUTF encoding, which encodes the NUL character as two bytes and supplementary characters (outside the BMP) as surrogate pairs rather than four-byte sequences. In practice, real-world save files are ordinary UTF-8 for everything you will meet in level.dat and playerdata; modern writers, including NBTBox, emit standard UTF-8.
The 16-bit length caps a single string at 65,535 bytes. Legitimate strings stay far below that; a length field pointing past the end of the file is a classic sign of corruption.
6. Compounds: named children and the End marker
A compound payload is a sequence of named tags, each written as:
type byte (1) | name length (2, unsigned) | name bytes (n) | payload
The sequence terminates with a single 0x00 byte — TAG_End. Note the asymmetry with lists: compounds are heterogeneous (each child declares its own type) and keyed by name. Duplicate names inside one compound are technically representable, but the game treats them as undefined behavior, and editors should avoid creating them.
7. Lists: one type, many items
A TAG_List payload is:
element type (1) | item count (4, signed) | item payloads (no names, no type bytes)
Three properties trip people up:
- Lists are homogeneous. Every item has the element type declared in the header. A list of compounds can store compounds and nothing else.
- Items are unnamed. Only the payload bytes are stored — no per-item type byte, no name. That is why lists are smaller than an equivalent array of one-key compounds.
- Empty lists still declare a type — usually type 0. Vanilla writes empty lists with element type TAG_End (0) and count 0. When a tool appends the first item to an empty list, it must promote the list to a real element type, exactly as the NBTBox editor does when you pick a type for the first item.
8. Arrays: ByteArray, IntArray, LongArray
The three array types share one layout: a signed 32-bit item count followed by the raw items, each of the fixed width of the element type (1, 4 and 8 bytes respectively). Unlike lists, arrays have no element type byte — the type is the tag type. Arrays are the storage of choice for bulk binary data: light levels and block data in chunk sections (ByteArray), block-state palettes and biome data (IntArray), and structure block data (LongArray).
A subtle point for byte values: NBT bytes are signed (−128…127), but most tooling, including hex editors and this site's array editor, displays them unsigned (0…255) because that is how raw bytes are conventionally read. Both views describe the same bits: a stored value of 255 appears as −1 in a signed read. NBTBox accepts either range on input and normalizes to the stored representation.
9. Compression containers: gzip, zlib, raw
The NBT specification defines the tree, not the envelope. On disk, the same payload travels in three containers, and a robust tool must handle all of them:
- gzip — what vanilla Java Edition writes for
level.dat,level.dat_old,playerdata/*.dat,structures/*.nbt,servers.datandmap_*.datfiles. Recognizable by the magic bytes1F 8B. - zlib — a zlib/deflate stream (
0x78as the first byte, with a valid header checksum) around the same payload. Some third-party tools and other games that adopted NBT use this variant; it is the format you get when someone exports NBT "for a server tool". - raw (uncompressed) — the payload as-is, starting with
0x0A. Common in in-memory contexts, network protocols and debugging output.
Detection is by header, and it matters for round-tripping: if you open a gzip file, you want the edited export to be gzip again so the game accepts it without a conversion step. NBTBox detects the container on open and keeps it on export by default, with a manual override in case you deliberately want to re-wrap the payload.
One related format worth distinguishing: region files (.mca/.mcr) store chunk NBT in 4 KiB sectors, each chunk compressed with raw DEFLATE and preceded by length and compression-type fields. That is a fourth container with its own framing — chunk data is NBT, but a region file is not itself an NBT document, which is why general NBT editors do not open .mca directly.
Also note the Bedrock edition writes an entirely different NBT dialect: little-endian numbers, variable-length string headers, and no name field in some contexts. Tools built for Java NBT, including this one, do not claim to read it.
10. What lives inside level.dat
Decompress a modern level.dat and you will find a root compound whose only child is Data. Notable fields inside:
| Tag | Type | Meaning |
|---|---|---|
| DataVersion | Int | Version stamp of the data format; drives the upgrade path when a newer game opens the world |
| LevelName | String | World name shown in the menu |
| RandomSeed | Long | The world seed |
| SpawnX / SpawnY / SpawnZ | Int | World spawn coordinates |
| GameType | Int | Default game mode (0 survival, 1 creative, 2 adventure, 3 spectator) |
| GameRules | Compound | Gamerule strings such as doDaylightCycle |
| Player | Compound | In single-player worlds, the host player's full state (in older saves; newer versions store it in playerdata) |
| version | Int | 19133 for worlds converted to the Anvil format |
The DataVersion integer deserves respect: it is how the game decides which upgrade steps to run. Changing it to a value the game does not expect is the fastest way to make a world unopenable — inspect it freely, but think twice before writing it.
11. SNBT: the text cousin of binary NBT
You may have met NBT in its text form inside commands, for example:
/give @p diamond_pickaxe{display:{Name:'{"text":"Einzelgänger"}'},Damage:37}
This string encoding is called SNBT (stringified NBT, sometimes "structured NBT"). It expresses the same tree — compounds in braces, lists in brackets, keys and typed values — but it is a separate syntax used in commands, data packs and function files. The binary format described above is what lives in files; SNBT is what lives in text. Converters between the two exist, but they are distinct representations, and this site's editor works directly on the binary form.
12. Pitfalls that break parsers
Every item below is a bug we have either hit or deliberately hardened against while building NBTBox:
- Wrong endianness. All Java NBT integers are big-endian. Reading a TAG_Int little-endian turns 3955 (
0x00000F73) into an astronomically large number. - Character length vs byte length in strings. Covered in §5 — measure encoded bytes, not code points.
- 64-bit precision loss. Reading TAG_Long into a double silently corrupts seeds and UUID halves above 253.
- Empty-list element type. Type 0 (TAG_End) with count 0 is valid; treating type 0 as an error rejects healthy files. Conversely, appending items without promoting the element type corrupts them.
- Unsigned/signed byte confusion. A ByteArray byte 0xFF is −1 as NBT. Array editors should state which convention they accept.
- Trailing bytes. A valid document ends exactly at the root compound's TAG_End. Extra trailing bytes indicate concatenation bugs or corruption; a strict parser rejects them rather than silently truncating.
- Assuming one compression format. A parser that only gunzips fails on zlib-wrapped payloads; one that only inflates fails on vanilla level.dat files. Detect, then decompress.
- NaN and float editing. IEEE-754 NaN values have many bit patterns; an editor that reparses floats through text can canonicalize them. It is another reason round-trip tests compare parsed trees and bytes, not printed strings.
13. See it for yourself
The fastest way to make this format concrete is to poke a real file. Take a level.dat from any of your worlds (make a copy first), open it in the NBTBox editor, and find the fields from §10 in the tree. Expand Data, change LevelName, export, and put the file back — the world list will show your new name. Then, if you want the full under-the-hood picture, decompress the file with any gzip tool and open the result in a hex editor: you will recognize 0A 00 00, the type bytes from §2, and the length-prefixed strings from §5, exactly as this page described them.
Further reference: the community-maintained specification on the Minecraft Wiki (search "NBT format") documents per-field usage inside the game, including every tag the game itself writes. This page focuses on the wire format itself — the part that is stable, public, and implementable by anyone.
Ready to edit? Open the NBT editor →