https://github.com/thenextlvl-net/nbt
A simple library to read and write NBT files
https://github.com/thenextlvl-net/nbt
minecraft-nbt nbt nbt-api nbt-files nbt-format nbt-library nbt-parser
Last synced: 11 days ago
JSON representation
A simple library to read and write NBT files
- Host: GitHub
- URL: https://github.com/thenextlvl-net/nbt
- Owner: TheNextLvl-net
- License: gpl-3.0
- Created: 2025-09-05T14:31:24.000Z (9 months ago)
- Default Branch: main
- Last Pushed: 2026-04-22T03:47:02.000Z (about 1 month ago)
- Last Synced: 2026-04-22T05:41:44.828Z (about 1 month ago)
- Topics: minecraft-nbt, nbt, nbt-api, nbt-files, nbt-format, nbt-library, nbt-parser
- Language: Java
- Homepage:
- Size: 603 KB
- Stars: 4
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# NBT
A small library for reading, writing, and (de)serializing Minecraft-like NBT (Named Binary Tag) data.
This project provides:
- Low-level streaming APIs to read/write NBT with GZIP compression: NBTInputStream and NBTOutputStream.
- A convenient file wrapper NBTFile for loading/saving a CompoundTag from/to disk.
- A flexible, pluggable serialization system (NBT facade) to convert between Java objects and Tag trees using
serializers/deserializers (aka adapters).
## Installation
Gradle (Kotlin DSL):
- Repository is published to https://repo.thenextlvl.net/#/releases/net/thenextlvl/nbt
- Group/artifact inferred from `build.gradle.kts`
```kts
repositories {
mavenCentral()
maven("https://repo.thenextlvl.net/releases/")
}
dependencies {
implementation("net.thenextlvl:nbt:3.0.0")
}
```
## Core concepts
- Tag: Base type for all NBT values (ByteTag, ShortTag, IntTag, LongTag, FloatTag, DoubleTag, StringTag, ByteArrayTag,
IntArrayTag, LongArrayTag, ListTag, CompoundTag). All tags know how to read/write themselves from/to streams.
- CompoundTag: A map of name → Tag. Commonly used as the root tag in files.
- NBTInputStream / NBTOutputStream: Low-level, GZIP-compressed streams for reading/writing tags. Strings are encoded
using the configured Charset (UTF-8 by default).
- NBTFile: Small utility to load/save a CompoundTag from a file path with charset handling.
- Serialization API: The NBT interface that converts between Tag and arbitrary Java objects using TagSerializer and
TagDeserializer (or a combined TagAdapter).
## Reading NBT files
You can read any NBT file using NBTInputStream. The stream transparently handles GZIP compression.
```java
import net.thenextlvl.nbt.NBTInputStream;
import net.thenextlvl.nbt.tag.CompoundTag;
import net.thenextlvl.nbt.tag.Tag;
import javax.swing.text.html.Option;
import java.io.FileInputStream;
import java.util.Map;
import java.util.Optional;
public class NBTExample {
public static void readData() throws Exception {
try (NBTInputStream input = new NBTInputStream(new FileInputStream("data.nbt"))) {
// Read the root entry (tag and optional name)
Map.Entry> entry = input.readNamedTag();
Tag root = entry.getKey();
String rootName = entry.getValue().orElse(null);
if (root instanceof CompoundTag compound) {
// Access values by name
var level = compound.get("Level");
var data = compound.getAsCompound("Data");
var list = compound.getAsList("Items");
}
}
}
}
```
> [!TIP]
> - If you only need the tag, use readTag().
> - Unknown tag IDs cause an IllegalArgumentException. You may register custom mappings on NBTInputStream via
registerMapping(typeId, function).
## Writing NBT files
Use NBTOutputStream to write a named root tag. The stream writes GZIP-compressed output.
```java
import net.thenextlvl.nbt.NBTOutputStream;
import net.thenextlvl.nbt.tag.CompoundTag;
import java.io.FileOutputStream;
public class NBTExample {
public static void writeData() throws Exception {
try (var out = new NBTOutputStream(new FileOutputStream("data.nbt"))) {
CompoundTag root = CompoundTag.builder()
.put("Name", "Example")
.put("Health", 20)
.put("Position", CompoundTag.builder()
.put("x", 1)
.put("y", 64)
.put("z", 1)
.build())
.build();
out.writeTag("Root", root); // name can be null
}
}
}
```
CompoundTag has a fluent Builder for convenience.
## Using NBTFile helper
If you prefer a small wrapper for file IO, NBTFile can load/save and retain the root name.
```java
import core.io.PathIO; // from net.thenextlvl.core:files
import net.thenextlvl.nbt.file.NBTFile;
import net.thenextlvl.nbt.tag.CompoundTag;
public static class NBTExample {
public static void writeData() throws Exception {
NBTFile file = new NBTFile<>(new PathIO(Path.of("data.nbt")), CompoundTag.empty());
CompoundTag root = file.get(); // loads if file exists, otherwise returns default root
String rootName = file.getRootName().orElse(null);
// modify root ...
root.add("Updated", true);
file.setRootName("Root");
file.save();
}
}
```
## Serialization: NBT facade
The serialization API turns Java objects into Tags and back. The NBT interface is the entry point. You configure an
instance via `NBT.builder()` and register (de)serializers or combined adapters.
Key types:
- `TagSerializer`: object -> Tag
- `TagDeserializer`: Tag -> object
- `TagAdapter`: both serializer and deserializer in one
- `TagSerializationContext` / `TagDeserializationContext`: provided to your (de)serializers for recursive (de)
serialization
An `NBT` instance comes with built-in adapters for common types:
- Primitives and boxed: boolean/Boolean, byte/Byte, short/Short, int/Integer, long/Long, float/Float, double/Double
- String, java.io.File, java.nio.file.Path, java.time.Duration, java.net.InetSocketAddress, java.util.UUID
### Quick start
```java
import net.thenextlvl.nbt.serialization.NBT;
import net.thenextlvl.nbt.tag.CompoundTag;
import net.thenextlvl.nbt.tag.Tag;
public record Player(String name, int level) {
public static void adapt() {
var nbt = NBT.builder().registerTypeAdapter(Player.class, new TagAdapter() {
@Override
public Tag serialize(Player player, TagSerializationContext ctx) {
return CompoundTag.builder()
.put("name", player.name())
.put("level", player.level())
.build();
}
@Override
public Player deserialize(Tag tag, TagDeserializationContext ctx) {
var root = tag.getAsCompound();
var name = root.get("name").getAsString();
var level = root.get("level").getAsInt();
return new Player(name, level);
}
}).build();
Tag asTag = nbt.serialize(new Player("Alex", 42));
Player back = nbt.deserialize(asTag, Player.class);
}
}
```
You can also register serializer and deserializer separately:
```java
NBT nbt = NBT.builder()
.registerTypeAdapter(Player.class, (TagSerializer) (player, context) -> {
return CompoundTag.builder()
.put("name", player.name())
.put("level", player.level())
.build();
})
.registerTypeAdapter(Player.class, (TagDeserializer) (tag, context) -> {
var root = tag.getAsCompound();
return new Player(
root.get("name").getAsString(),
root.get("level").getAsInt()
);
})
.build();
```
If you need polymorphic handling, register a hierarchy adapter so it also applies to subtypes:
```java
NBT nbt = NBT.builder().registerTypeHierarchyAdapter(Animal.class, new AnimalAdapter()).build(); // applies to all subclasses
```
During (de)serialization, you can call `context.serialize(object)` and `context.deserialize(tag, type)` from within your
custom adapters to handle nested fields using already registered adapters.
## Creating a custom serializer with the NBT class
This example shows how to write a dedicated adapter for a complex type containing nested objects and collections.
```java
import net.thenextlvl.nbt.serialization.*;
import net.thenextlvl.nbt.tag.*;
import java.util.List;
record Position(int x, int y, int z) {
}
record InventoryItem(String id, int count) {
}
record PlayerData(String name, Position pos, java.util.List items) {
}
class PositionAdapter implements TagAdapter {
@Override
public Tag serialize(Position position, TagSerializationContext context) {
return CompoundTag.builder()
.put("x", position.x())
.put("y", position.y())
.put("z", position.z())
.build();
}
@Override
public Position deserialize(Tag tag, TagDeserializationContext context) {
var root = tag.getAsCompound();
return new Position(
root.get("x").getAsInt(),
root.get("y").getAsInt(),
root.get("z").getAsInt()
);
}
}
class InventoryItemAdapter implements TagAdapter {
@Override
public Tag serialize(InventoryItem item, TagSerializationContext context) {
return CompoundTag.builder()
.put("id", item.id())
.put("count", item.count())
.build();
}
@Override
public InventoryItem deserialize(Tag tag, TagDeserializationContext context) {
var root = tag.getAsCompound();
return new InventoryItem(
root.get("id").getAsString(),
root.get("count").getAsInt()
);
}
}
class PlayerDataAdapter implements TagAdapter {
@Override
public Tag serialize(PlayerData data, TagSerializationContext context) throws ParserException {
var builder = ListTag.builder()
.contentType(CompoundTag.ID);
for (var it : data.items()) {
// Let context use InventoryItemAdapter
builder.add(context.serialize(it));
}
return CompoundTag.builder()
.put("name", data.name())
.put("pos", context.serialize(data.pos())) // delegate to PositionAdapter
.put("items", builder.build())
.build();
}
@Override
public PlayerData deserialize(Tag tag, TagDeserializationContext context) throws ParserException {
var c = tag.getAsCompound();
var name = c.get("name").getAsString();
var pos = context.deserialize(c.get("pos"), Position.class);
var listTag = c.getAsList("items");
final var items = new java.util.ArrayList(listTag.size());
for (final var t : listTag) {
items.add(context.deserialize(t, InventoryItem.class));
}
return new PlayerData(name, pos, List.copyOf(items));
}
}
var nbt = NBT.builder()
.registerTypeAdapter(Position.class, new PositionAdapter())
.registerTypeAdapter(InventoryItem.class, new InventoryItemAdapter())
.registerTypeAdapter(PlayerData.class, new PlayerDataAdapter())
.build();
var data = new PlayerData("Alex", new Position(1, 64, 1), List.of(new InventoryItem("minecraft:stone", 32)));
Tag tag = nbt.serialize(data);
PlayerData back = nbt.deserialize(tag, PlayerData.class);
```
> [!TIP]
> - Use `CompoundTag.Builder` to construct compound values fluently.
> - `ListTag` stores tags only; use the context to convert elements.
> - Throw `ParserException` in your (de)serializers to signal invalid data.
## Registering custom tag type mappings for reading
If you introduce your own `Tag` implementation with a custom type ID, you can teach NBTInputStream how to read it:
```java
public static void createCustomTag() throws Exception {
final NBTInputStream input = new NBTInputStream(new FileInputStream("data.nbt"));
input.registerMapping(MyCustomTag.ID, MyCustomTag::read);
}
```
`NBTOutputStream` will call `Tag#write` on whatever Tag you pass to `writeTag`.