====== 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 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> 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> 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> 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 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 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 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.