Skip to content

Endecs

Outdated Documentation

This version of the Endec documentation, while still communicating the correct concepts, does no longer directly apply to the versions of the API which is presently available in owo. If you are unsure about something, try to reference the JavaDoc or simply come ask in our Discord - we'll be happy to help

The Endec API is a format-agnostic serialization framework conceptually related to serde and interoperable with Mojang's own Codec API (found in DFU and used throughout the vanilla codebase). Given the endec for a certain type, it can be serialized to and from any format with a (De)Serializer implementation.

Importantly, endecs can serialize to both sequential and hierarchical formats - this means that, contrary to DFU's Codec, endecs can be used directly for networking without needing an intermediary format like NBT and wasting a bunch of space on structure information.

The Data Model

The framework models all possible serialized formats using the following 11 primitive and 4 compound types which each (de)serializer must support. This table lists the types with their representations in NBT and JSON:

TypeDescriptionNBTJSON
byteSigned 8-bit integerNbtByteJsonPrimitive
shortSigned 16-bit integerNbtShortJsonPrimitive
intSigned 32-bit integerNbtIntJsonPrimitive
var_intSigned 32-bit variable-length integerNbtIntJsonPrimitive
longSigned 32-bit integerNbtLongJsonPrimitive
var_longSigned 64-bit variable-length integerNbtLongJsonPrimitive
floatSingle-precision floating point numberNbtFloatJsonPrimitive
doubleDouble-precision floating point numberNbtDoubleJsonPrimitive
booleantrue or falseNbtByteJsonPrimitive
stringA UTF-8 character sequenceNbtStringJsonPrimitive
bytesA sequence of bytesNbtByteArrayJsonArray
optionalA single value or nothingNbtCompoundJsonElement | JsonNull
sequenceSequence of values, each of the same typeNbtListJsonArray
mapMapping from strings to single valuesNbtCompoundJsonObject
structMapping from strings to single values with all keys statically knownNbtCompoundJsonObject

Optional fields in JSON and NBT

For both JSON and NBT, when the optional being serialized is the field of a struct, the field will be omitted to indicate an empty optional and simply be the value otherwise

Using Endecs

The framework comes with a number of built-in endecs ready for you to use - the 11 primitive types from the data model accessible as constants on the Endec interface and the rest in the BuiltInEndecs class. Additionally, any DFU codec, vanilla or modded, can be used through Endec.ofCodec

Now, say we want to encode a single BlockPos as JSON. For this, we use the endec supplied in BuiltInEndecs, the JsonSerializer class and the Endec#encodeFully method - like this:

java
var pos = new BlockPos(1, 2, 3);
JsonElement result = BuiltInEndecs.BLOCK_POS.encodeFully(JsonSerializer::of, pos);

If you now printed this result, it'd look something like this: [1,2,3]. Next, let's turn this JSON back into a BlockPos. For this we use the same endec as we used for encoding, the JsonDeserializer class and the Endec#decodeFully method:

java
BlockPos decoded = BuiltInEndecs.BLOCK_POS.decodeFully(JsonDeserializer::of, result);

Unlike DFU, if an endec encounters an error while decoding it throws an exception - so make sure you catch those if you're deserializing unknown data.

Building Endecs

Of course, often times you'll be serializing your own types for which no endec exists yet - thus we must also know how to construct ones of our own.

Basic compound types

The 3 simple trivial compound types from the data model all have direct operators on each endec. For example, we can easily turn an Endec<UUID> into an Endec<List<UUID>> with Endec#listOf(). Generally:

MethodType Signature
Endec#listOfEndec<T> 🠖 Endec<List<T>>
Endec#mapOfEndec<T> 🠖 Endec<Map<String, T>>
Endec#optionalOfEndec<T> 🠖 Endec<Optional<T>>

Using these, you can realize a lot of basic data structures.

For maps with non-string keys, you have two options

  • If your keys have a good string representation, use Endec.map(Function, Function, Endec) which encodes to a proper data model map by serializing each key using the provided keyToString and keyFromString functions. For example, for a map from Identifier to boolean, you might do something like this:

    java
    Endec<Map<Identifier, Boolean>> endec = Endec.map(Identifier::toString, Identifier::new, Endec.BOOLEAN);
  • Otherwise, use Endec.map(Endec, Endec) and supply an endec for your keys. Since the data model cannot represent a map like this without somehow requiring a string representation for the keys, this instead serializes to a list of key-value pairs. This example is quite contrived but still demonstrates the principle:

    java
    Endec<Map<BlockPos, Identifier>> endec = Endec.map(BuiltInEndecs.BLOCK_POS, BuiltInEndecs.IDENTIFIER);

Structs

Assume we have the following class which we wish to serialize:

java
public class FabledBananasClass {
    private final int bananaAmount;
    private final Item bananaItem;
    private final List<BlockPos> bananaPositions;

    public FabledBananasClass(int bananaAmount, Item bananaItem, List<BlockPos> bananaPositions) {...}

    public int bananaAmount() { return this.bananaAmount; }
    public Item bananaItem() { return this.bananaItem; }
    public List<BlockPos> bananaPositions() { return this.bananaPositions; }
}

For this, we want to use the 4th compound type from the data model - structs. There are two reasons for this: For one, the API is specifically designed for this and thus very easy to use - but also, binary formats like the ByteBufSerializer (used for networking) can omit the field names and save a bunch of space this way over just using a map.

To create an endec for a struct, we first need endecs for all its fields. In this case, those are reasonably simple to obtain:

  • For bananaAmount we use Endec.INT
  • Since the values of bananaItem are stored in the item registry, we can serialize them as their identifiers using BuiltInEndecs.ofRegistry(Registries.ITEM)
  • Finally, for bananaPositions we use the listOf operator from above: BuiltInEndecs.BLOCK_POS.listOf()

Now we may proceed using the aptly named StructEndecBuilder and the fieldOf operator which turns an endec into a struct field specification:

java
Endec<FabledBananasClass> thisEndecIsBananas = StructEndecBuilder.of(
        Endec.INT.fieldOf("banana_amount", FabledBananasClass::bananaAmount),
        BuiltInEndecs.ofRegistry(Registries.ITEM).fieldOf("banana_item", FabledBananasClass::bananaItem),
        BuiltInEndecs.BLOCK_POS.listOf().fieldOf("banana_positions", FabledBananasClass::bananaPositions),
        // up to 17 fields can be declared here (1)
        FabledBananasClass::new
);
  1. It's 17 specifically because that is one more than DFU supports. Take that!

For each field, we specify the name to use in the serialized representation and a getter function the endec should use for getting the value of that field from an instance of the class. Also, we supply a constructor the endec uses for instantiating the class when decoding, which accepts the same fields in the same order as declared.

If you wanted any of these fields to be optional, you would use the optionalFieldOf operator for that field instead and supply a default value to use when the field is missing from the serialized data.

Optional fields are not always supported

Optional fields are generally only supported by self-described formats. The packet buffer format, for example, does not support optional fields since it omits the fields names and thus cannot tell whether a field is missing.

Should a field be missing there, deserialization failure will generally result. However, since those formats are not usually authored by humans, this should never be an issue in practice


The structure and bananas of this guide are somewhat inspired by Evelyn's excellent Codec guide