Codecs
Codecs are an abstraction layer around DynamicOps which allow objects to be serialized and deserialized in different contexts such as JSON or NBT. It creates an easy way to read and interpret objects without the need of manual labor.
Codec Serialization and Deserialization
Codec Serialization and Deserialization is handled through two main methods: Codec#encodeStart
and Codec#parse
respectively. Each of these methods returns a DataResult
which holds the encoded object type or the decoded object class. An Optional
of the resulting output can be grabbed via DataResult#result
. If a custom message should be specified along with the error message, that can be specified using DataResult#resultOrPartial
. Alternatively,DataResult#getOrThrow
can be used which grabs the instance directly instead of an optional.
Creating a Codec for a Class
Let's assume there is the following class structure that a codec should be created for:
public class ExampleCodecClass { private final int field_1; private final List<BlockPos> field_2; private final Item field_3; public ExampleCodecClass(int field_1, List<BlockPos> field_2, Item field_3) {...} }
For each basic object instance, a codec can be constructed using RecordCodecBuilder::create
. This takes in a function that converts an Instance
of an object, which is a group of codecs for each serializable field, to an App
, which is an unary type constructor for allowing algorithms to be generalized using generics.
public static final Codec<ExampleCodecClass> CODEC = RecordCodecBuilder.create(builder -> { return ...; });
To add a list of valid codecs, which is denoted by P
where n is the number of fields in the instance,Instance#group
is used which takes in codecs converted into an App
of some kind. This example will examine three such scenarios.
First, there is a primitive integer field. All primitive codecs are declared within the Codec
class along with a few extra primitive streams (in this case we will use Codec#INT
). To convert this codec into a valid key-pair form, the parameter name needs to be specified. This can be done using Codec#fieldOf
which will take in a string which represents the key of this field. This will convert the codec into a MapCodec which as the name states creates a key-value pair to deserialize the instance from. From there, how to serialize the instance from the class object must also be specified. This can be done using MapCodec#forGetter
which takes in a function that converts the class object to the type instance, hence the getter method name. This creates a RecordCodecBuilder
which will be the final state of the codec as it is an instance of App
.
public static final Codec<ExampleCodecClass> CODEC = RecordCodecBuilder.create(builder -> { return builder.group(Codec.INT.fieldOf("field_1").forGetter(obj -> obj.field_1), ...)...; });
Next, there is a list of BlockPos
which has a premade codec within itself. However, a Codec
needs to be converted into a Codec
. Luckily, there are a few helpers within the codec class that allows some of these conversions to be trivial. In this case, Codec#listOf
will convert a codec of some generic into a list of that generic. The process for attaching the codec is exactly the same.
public static final Codec<ExampleCodecClass> CODEC = RecordCodecBuilder.create(builder -> { return builder.group(Codec.INT.fieldOf("field_1").forGetter(obj -> obj.field_1), BlockPos.CODEC.listOf().fieldOf("field_2").forGetter(obj -> obj.field_2), ...)...; });
A few other notable mentions that might be used within a codec:
Method | Description |
intRange | Creates a integer codec with a valid inclusive range. |
floatRange | Creates a float codec with a valid inclusive range. |
doubleRange | Creates a double codec with a valid inclusive range. |
pair | Create a pair using two codecs. |
either | Creates an either (an object with some fallback object) using two codecs. |
unboundedMap | Creates a map using two codecs. |
Last, there is a Item. This is optional and should default to Items#AIR
when not defined. Here, there will be two techniques used to grab the associated codec. By default, a Registry
is an instance of a codec. Therefore, the codec can be grabbed by specifying the registry instance (e.g. Registry#ITEM
). What if there is no vanilla registry instance however? Then, another codec method can be used: Codec#xmap
. This allows the associated object to be mapped to another object. A function specifies mapping the associated object to the new object for decoding and vice versa for encoding. For example, the ResourceLocation
codec can be mapped to an Item
through the forge registry instance.
To define a field as optional, Codec#optionalFieldOf
should be used. One instance holds the value as an Optional
while the other allows a defined default value.
public static final Codec<ExampleCodecClass> CODEC = RecordCodecBuilder.create(builder -> { return builder.group(Codec.INT.fieldOf("field_1").forGetter(obj -> obj.field_1), BlockPos.CODEC.listOf().fieldOf("field_2").forGetter(obj -> obj.field_2), ResourceLocation.CODEC.xmap(loc -> ForgeRegistries.ITEMS.getValue(loc), item -> item.getRegistryName()).optionalFieldOf("field_3", Items.AIR).forGetter(obj -> obj.field_3))...; });
Now, there is a product P3
as there is three parameters. To convert this into an App
, the method P#apply
should be called. This takes in an Applicative
which our builder is an instance of and a function that returns the class object given the specified wrapped arguments. Creating a new constructor is one way of creating the outputted codec for the class object.
public static final Codec<ExampleCodecClass> CODEC = RecordCodecBuilder.create(builder -> { return builder.group(Codec.INT.fieldOf("field_1").forGetter(obj -> obj.field_1), BlockPos.CODEC.listOf().fieldOf("field_2").forGetter(obj -> obj.field_2), ResourceLocation.CODEC.xmap(loc -> ForgeRegistries.ITEMS.getValue(loc), item -> item.getRegistryName()).optionalFieldOf("field_3", Items.AIR).forGetter(obj -> obj.field_3)) .apply(builder, ExampleCodecClass::new); });
Limitations
Codecs are required to abide by a String-Object key-pair. Any codec that does not have a String key will throw an error during encoding and decoding.
A group can have at most 16 inner codecs normally. This limitation is specified by the number of product generic classes available.