Minecraft's NBT format for Go, in two parts:
- A library modeled on
encoding/json, with astructurepackage for Java Edition structure (.nbt) files. It is documented below. nbt-mcp, an MCP server built on the library that lets an AI model build, inspect and save structure files. It has its own README.
go get github.com/MinecraftMetascript/nbt-go@latestRequires Go 1.22 or newer. The library has no dependencies outside the standard library.
Marshal/Unmarshal,Encoder/Decoder, andnbt:"name,omitempty"struct tags, the same wayencoding/jsondoes it.- A
Tagtree (Compound,List,Int, …) for data whose shape you don't know ahead of time, likemap[string]anyfor JSON. - Gzip and zlib compressed input is detected automatically, and
Encoder.SetCompressionwrites it. - SNBT (the
{id:"minecraft:stone",count:1b}syntax used in commands) both ways. - The
structurepackage reads, writes and builds structure files.
type Item struct {
Slot int8 `nbt:"Slot"`
ID string `nbt:"id"`
Count int32 `nbt:"count"`
}
type Chest struct {
CustomName string `nbt:"CustomName,omitempty"`
Items []Item `nbt:"Items"`
}
data, err := nbt.Marshal(Chest{Items: []Item{{0, "minecraft:diamond", 3}}})
var chest Chest
err = nbt.Unmarshal(data, &chest)Reading and writing files:
f, _ := os.Open("level.dat")
dec := nbt.NewDecoder(f) // gzip is detected
var level struct {
Data struct {
LevelName string
SpawnX, SpawnY, SpawnZ int32
}
}
err := dec.Decode(&level)
enc := nbt.NewEncoder(out)
enc.SetCompression(nbt.Gzip)
err = enc.Encode(level)Unknown data, or data you want to pass through untouched:
var tree nbt.Tag // or nbt.Compound, or any
nbt.Unmarshal(data, &tree)
fmt.Println(tree) // prints SNBT: {Items:[{Slot:0b,count:3,id:"minecraft:diamond"}]}SNBT:
var chest Chest
err := nbt.UnmarshalSNBT(`{Items:[{Slot:0b,id:"minecraft:diamond",count:3}]}`, &chest)
s, err := nbt.MarshalSNBT(chest)NBT paths, the foo.bar[0] syntax of /data, with the game's rules for
matching, creating missing parents and "Nothing changed":
root := nbt.Compound{}
p := nbt.MustParsePath(`Items[{Slot:0b}].count`)
n, err := p.Set(root, nbt.Int(5)) // creates Items:[{Slot:0b,count:5}]; n is 1
tags := p.Get(root) // [5]
nbt.MustParsePath("Items[0]").Remove(root)nbt.Quote quotes a string the way the game writes it in SNBT, and Equal,
Matches and Clone compare and copy tag trees.
| Go | NBT |
|---|---|
bool |
TAG_Byte (0/1) |
int8, uint8 |
TAG_Byte |
int16, uint16 |
TAG_Short |
int32, uint32, int, uint |
TAG_Int (int/uint error if out of range) |
int64, uint64 |
TAG_Long |
float32 / float64 |
TAG_Float / TAG_Double |
string |
TAG_String |
[]byte, []int8 |
TAG_Byte_Array |
[]int32, []uint32 |
TAG_Int_Array |
[]int64, []uint64 |
TAG_Long_Array |
| other slices and arrays | TAG_List (elements must share one type) |
map[string]T, structs |
TAG_Compound |
| pointers, interfaces | the value they point to; nil is omitted |
nbt.Tag types (nbt.Compound, …) |
themselves |
nbt.Marshaler / encoding.TextMarshaler |
whatever MarshalNBT returns / TAG_String |
Struct tag options:
omitemptyleaves out zero values, as inencoding/json.listwrites a byte, int or long slice as aTAG_Listinstead of an array. Minecraft uses lists for things like positions:Pos [3]int32 `nbt:"pos,list"`.-skips the field.
Decoding is lenient where it's safe: any integer tag decodes into any Go integer or bool if the value fits, arrays and lists decode into slices, and unknown keys are ignored unless you call Decoder.DisallowUnknownFields. Errors report the path to the bad value, such as blocks[3].pos[1].
s := structure.New(5023) // the DataVersion of the target Minecraft version; 5023 is 26.3
s.Fill(0, 0, 0, 4, 0, 4, structure.MustParseBlockState("stone_bricks"))
s.SetBlock(2, 1, 2, structure.MustParseBlockState("chest[facing=north]"),
nbt.Compound{"LootTable": nbt.String("minecraft:chests/simple_dungeon")})
s.AddEntity([3]float64{2.5, 1, 0.5}, nbt.Compound{"id": nbt.String("minecraft:armor_stand")})
f, _ := os.Create("my_structure.nbt")
defer f.Close()
err := s.Write(f) // gzip compressed, ready for a structure block or data packstructure.Read loads existing files, including vanilla ones with multiple palettes (shipwrecks and similar). Positions with no block are left alone when the structure is placed, like minecraft:structure_void; place minecraft:air to clear a space.
The DataVersion decides how the file is written. Minecraft 26.3 renamed the palette keys from Name/Properties to id/properties (from DataVersion 5009 on). Both are read, and a structure is written with the keys that match its DataVersion, so set it to the version of the game that will load the file. Newer games upgrade older files, but older games can't read newer ones.
The repository holds two Go modules: the library at the root and the server in cmd/nbt-mcp, which pins a version of the library in its go.mod. Run the checks in each:
go test -race ./...
golangci-lint run # in cmd/nbt-mcp: golangci-lint run -c ../../.golangci.ymlTo build the server against a local copy of the library, add a go.work file (it is not committed):
go work init . ./cmd/nbt-mcpThis library covers Java Edition NBT (big-endian). Bedrock's little-endian and network variants, and region (.mca) files, aren't supported yet.
MIT. See LICENSE.