User Tools

Site Tools


tutorial:codec

Using codecs (DRAFT)

What is a codec

A codec, introducted in Java Edition 1.16, is a specification of conversion between any type of object (such as LootTable, Advancement or BlockPos) and any type serialized from (nbt, json, etc.). A codec is a combination of encoder and decoder. An encoder encodes an object to serialized form, and a decoder decodes the serialized from into objects.

For example, loot tables are written in json forms in data packs, and are loaded as LootTable objects in the server. To load loot tables from the jsons in the data pack, the codec is used. There are many pre-written codecs in Minecraft, each of which, is used for a specific type of object, but can serialize or deserialize between it and different types of serialized from. For example, LootTable.CODEC can convert LootTable objects into jsons and nbts, and can also convert jsons or nbts into LootTable objects.

Codec was introduced in 1.16, but has been increasingly widely used in Minecraft since 1.20. For example, LootTable, Advancements and Text, previously serialized or deserialized in other manners in older versions, but now using codecs. Item components, introduced in 1.20.5, also use codecs to serialize.

How to use a codec

When you serialize or deserialize, you need to specify a DynamicOps, which specifies each concrete action in the serialization or deserialization. Mose common used are JsonOps.INSTANCE and NbtOps.INSTANCE. The following code takes an example of converting between BlockPos and NbtElement.

    // serializing a BlockPos
    final BlockPos blockPos = new BlockPos(1, 2, 3);
    final DataResult<NbtElement> encodeResult = BlockPos.CODEC.encodeStart(NbtOps.INSTANCE, blockPos);
 
    // deserializing a BlockPos
    final NbtList nbtList = new NbtList();
    nbtList.add(NbtInt.of(1));
    nbtList.add(NbtInt.of(2));
    nbtList.add(NbtInt.of(3));
    final DataResult<Pair<BlockPos, NbtElement>> decodeResult = BlockPos.CODEC.decode(NbtOps.INSTANCE, nbtList);

As seen in the code, the result is not directly NbtElement or BlockPos, but a DataResult wrapping them. That's because errors are common in serialization, and instead of exceptions, errors in the data results often happens. You can fetch the result with result() (which may be empty when error happens), or fecth the error message with error() (which may be empty when error does not happen). You can also directly fetch the result with getOrThrow() (or Util.getResult in older versions).

    // get the result when succeed
    final NbtList nbtList = new NbtList();
    nbtList.add(NbtInt.of(1));
    nbtList.add(NbtInt.of(2));
    nbtList.add(NbtInt.of(3));
    final DataResult<Pair<BlockPos, NbtElement>> result1 = BlockPos.CODEC.decode(NbtOps.INSTANCE, nbtList);
    System.out.println(result1.getOrThrow().getFirst());
 
    // get the result when error
    final NbtString nbtString = NbtString.of("can't decode me");
    final DataResult<Pair<BlockPos, NbtElement>> result2 = BlockPos.CODEC.decode(NbtOps.INSTANCE, nbtString);
    System.out.println(result2.error().get().message());

There is a special type of DynamicOps, named RegistryOps, which is usually created with RegistryWrapper.getOps. It is a wrapper of other ops, which means, when encoding or decoding, it behaves basically identical to the wrapped ops, but have some differences: when encoding or decoding some registry entries, it will use the specified RegistryWrapper to get objects or entries, while other ops may directly use the registry or even throw an error.

How to write a codec

Mapping existing codec

For simple objects, you can directly convert it between those have existing codecs. Mojang provides codecs for all primitive types and common types. In this example, you want to create a codec for Identifier. We just know that it can be converted from and to a String, then we map through xmap:

    Codec<Identifier> identifierCodec = Codec.STRING.xmap(s -> new Identifier(s), identifier -> identifier.toString());

When decoding, the serialized from is converted into string through Codec.STRING, and then converted into Identifier through the first lambda. When encoding, the Identifier is converted into string through the second lambda, and then encoded through Codec.STRING.

If the serialized from is something that cannot be converted into a string, a codec result may be DataResult.Error, and handled properly as expected. However, if you pass a string that cannot be an Identifier, such as NbtString.of(“ABC”), decoding it will directly throw InvalidIdentifierException. Therefore, to handle errors properly, we use flatXmap:

    Codec<Identifier> identifierCodec = Codec.STRING.flatXmap(s -> {
      try {
        return DataResult.success(new Identifier(s));
      } catch (InvalidIdentifierException e) {
        return DataResult.error(() -> "The identifier is invalid:" + e.getMessage());
      }
    }, identifier -> DataResult.success(identifier.toString()));

Note that this time we use flatXmap instead of xmap, in which, the two lambdas returns DataResult. This is used then two lambdas may return failed results. In this case, the second lambda does not fail, so we can also use comapFlatMap:

    Codec<Identifier> identifierCodec = Codec.STRING.comapFlatMap(s -> {
      try {
        return DataResult.success(new Identifier(s));
      } catch (InvalidIdentifierException e) {
        return DataResult.error(() -> "The identifier is invalid:" + e.getMessage());
      }
    }, identifier -> identifier.toString());

Record codec

Most objects are complicated, which cannot simply represented as a primitive type. It may have multiple fields, and stored as a map-lik object, such as NbtCompound or JsonObject. In this case, you need to specify the fields, including name and codecs of the fields.

Dispatching codec

Some objects may have different types. In the serialized form, it may need a “type”, and according to the “type”, specify a codec to deserialize or serialize.

tutorial/codec.txt · Last modified: 2024/05/30 06:22 by solidblock