Skip to main content
Version: 1.21.6 - 1.21.7

Value I/O

The Value I/O system is a standardized serialization method to manipulate data of some backing object, such as CompoundTags for NBT.

Inputs and Outputs

The Value I/O system is made up of two parts: a ValueOutput that writes to the object during serialization, and a ValueInput that reads from the object during deserialization. Implementing methods typically take in the ValueOutput or ValueInput as its only parameter, returning nothing. The value I/O expects the backing object to be a dictionary of string keys to object values. Using the provided methods, the value I/O then reads or writes information to the backing object.

// For some BlockEntity subclass
@Override
protected void saveAdditional(ValueOutput output) {
super.saveAdditional(output);
// Write data to the output
}

@Override
protected void loadAdditional(ValueInput input) {
super.loadAdditional(input);
// Read data from the input
}

// For some Entity subclass
@Override
protected void addAdditionalSaveData(ValueOutput output) {
super.addAdditionalSaveData(output);
// Write data to the output
}

@Override
protected void readAdditionalSaveData(ValueInput input) {
super.readAdditionalSaveData(input);
// Read data from the input
}

Primitives

Value I/O contains methods for reading and writing certain primitives. ValueOutput methods are prefixed with put*, taking in the key and the primitive value. ValueInput methods are named as get*Or, taking in the key and a default if none is present.

Java TypeValueOutputValueInput
booleanputBooleangetBooleanOr
byteputBytegetByteOr
shortputShortgetShortOr
intputIntgetInt*, getIntOr
longputLonggetLong*, getLongOr
floatputFloatgetFloatOr
doubleputDoublegetDoubleOr
StringputStringgetString*, getStringOr
int[]putIntArraygetIntArray*

* These ValueInput methods return an Optional-wrapped primitive instead of taking and passing back some fallback.

// For some BlockEntity subclass
@Override
protected void saveAdditional(ValueOutput output) {
super.saveAdditional(output);

// Write data to the output
output.putBoolean(
// The string key
"boolValue",
// The value associated with this key
true
);
output.putString("stringValue", "Hello world!");
}

@Override
protected void loadAdditional(ValueInput input) {
super.loadAdditional(input);

// Read data from the input

// Defaults to false if not present
boolean boolValue = input.getBooleanOr(
// The string key to retrieve
"boolValue",
// The default value to return if the key is not present
false
);

// Defaults to 'Dummy!' if not present
String stringValue = input.getStringOr("stringValue", "Dummy!");
// Returns an optional-wrapped value
Optional<String> stringValueOpt = input.getString("stringValue");
}

Codecs

Codecs can also be used to store and read values from the value I/O. In vanilla, all Codecs are handled using a RegistryOps, allowing the storage of datapack entries. ValueOutput#store and storeNullable take in the key, the codec to write the object, and the object itself. storeNullable will not write anything if the object is null. ValueInput#read can read the object by taking in the key and the codec, returning an Optional-wrapped object.

// For some BlockEntity subclass
@Override
protected void saveAdditional(ValueOutput output) {
super.saveAdditional(output);

// Write data to the output
output.storeNullable("codecValue", Rarity.CODEC, Rarity.EPIC);
}

@Override
protected void loadAdditional(ValueInput input) {
super.loadAdditional(input);

// Read data from the input
Optional<Rarity> codecValue = input.read("codecValue", Rarity.CODEC);
}

ValueOutput and ValueInput also provide a store / read method for MapCodecs. Compared to the Codec, the MapCodec variant merges the values onto the current root.

// For some BlockEntity subclass
@Override
protected void saveAdditional(ValueOutput output) {
super.saveAdditional(output);

// Write data to the output
output.store(
SingleFile.MAP_CODEC,
new SingleFile(ResourceLocation.fromNamespaceAndPath("examplemod", "example"))
);
}

@Override
protected void loadAdditional(ValueInput input) {
super.loadAdditional(input);

// Read data from the input

// No key is needed as they are stored on the root value access
Optional<SingleFile> file = input.read(SingleFile.MAP_CODEC);
// This is present as `SingleFile` writes the `resource` parameter
String resource = input.getStringOr("resource", "Not present!");
}
warning

The MapCodec will write any keys to the value access, potentially overwriting existing data. Make sure that any keys within the MapCodec are distinct from other keys.

Lists

Lists can be created and read from through one of two methods: child value I/Os or [Codecs].

A list is created via ValueOutput#childrenList, taking in some key. This returns a ValueOutput.ValueOutputList, which acts as a write-only list of value objects. A new value object can be added to the list via ValueOutputList#addChild. This returns a ValueOutput to write the value object data to. The list can then be read using ValueInput#childrenList, or childrenListOrEmpty to default to an empty list when not present. These methods return a ValueInput.ValueInputList, which acts as a read-only iterable or stream (via stream).

// For some BlockEntity subclass
@Override
protected void saveAdditional(ValueOutput output) {
super.saveAdditional(output);

// Write data to the output

// Create List
ValueOutput.ValueOutputList listValue = output.childrenList("listValue");
// Add elements
ValueOutput childIdx0 = listValue.addChild();
childIdx0.putBoolean("boolChild", false);
ValueOutput childIdx1 = listValue.addChild();
childIdx1.putInt("boolChild", true);
}

@Override
protected void loadAdditional(ValueInput input) {
super.loadAdditional(input);

// Read data from the input

// Read values of list
for (ValueInput childInput : input.childrenListOrEmpty("listValue")) {
boolean boolChild = childInput.getBooleanOr("boolChild", false);
}
}

Codecs provide a list variant for data objects via ValueOutput#list. This takes in a key and some Codec, returning a ValueOutput.TypedOutputList. A TypedOutputList is the same as ValueOutputList, except it operates on the data object instead of using another value I/O. Elements can be added to the list via TypedOutputList#add. Then, similarly, the list can then be read using ValueInput#list or listOrEmpty, returning a TypedValueInput.

note

The main difference between a TypedValueOutput / TypedValueInput and a Codec#listOf is how errors are handled. For a Codec#listOf, a failed entry will result in the entire object being marked as an error DataResult. Meanwhile, a typed value I/O handles the error typically through a ProblemReporter. In vanilla, Codec#listOf provides more flexibility since ProblemReporters are specified when creating the value I/O. However, custom value I/O usage can implement either depending on the use case.

// For some BlockEntity subclass
@Override
protected void saveAdditional(ValueOutput output) {
super.saveAdditional(output);

// Write data to the output

// Create List
ValueOutput.TypedInputList<Rarity> listValue = output.list("listValue", Rarity.CODEC);
// Add elements
listValue.add(Rarity.COMMON);
listValue.add(Rarity.EPIC);
}

@Override
protected void loadAdditional(ValueInput input) {
super.loadAdditional(input);

// Read data from the input

// Read values of list
for (Rarity rarity : input.listOrEmpty("listValue", Rarity.CODEC)) {
// ...
}
}
warning

Lists are still written to the ValueOutput even when empty. If you don't want to write the list, then the TypedOutputList or ValueOutputList should check if it isEmpty, then call discard with the list key.

// For some BlockEntity subclass
@Override
protected void saveAdditional(ValueOutput output) {
super.saveAdditional(output);

// Write data to the output

// Create List
ValueOutput.TypedInputList<Rarity> listValue = output.list("listValue", Rarity.CODEC);

// Check if list is empty
if (listValue.isEmpty()) {
// Discard from output
output.discard("listValue");
}
}

Objects

Objects can be created and read from via children. ValueOutput#child creates a new ValueObject given a key. Then, the object can be read using ValueInput#child, or childOrEmpty if it should default to an ValueInput with an empty backing value.

// For some BlockEntity subclass
@Override
protected void saveAdditional(ValueOutput output) {
super.saveAdditional(output);

// Write data to the output

// Create object
ValueOutput objectValue = output.child("objectValue");
// Add data to object
objectValue.putBoolean("boolChild", true);
objectValue.putInt("intChild", 20);
}

@Override
protected void loadAdditional(ValueInput input) {
super.loadAdditional(input);

// Read data from the input

// Read object
ValueInput objectValue = input.childOrEmpty("objectValue");
// Get data from object
boolean boolChild = objectValue.getBooleanOr("boolChild", false);
int intChild = objectValue.getIntOr("intChild", 0);
}

ValueIOSerializable

ValueIOSerializable is a NeoForge-added interface for objects that can be serialized and deserialized using value I/Os. NeoForge uses this API to handle data attachments. The interface provides two methods: serialize to write the object to a ValueOutput, and deserialize to read the object from a ValueInput.

public class ExampleObject implements ValueIOSerializable {

@Override
public void serialize(ValueOutput output) {
// Write the object data here
}

@Override
public void deserialize(ValueInput input) {
// Read the object data here
}
}

Implementations

NBT

Value I/O for NBTs is handled via TagValueOutput and TagValueInput.

A TagValueOutput can be created via createWithContext or createWithoutContext, createWithContext means that the output has access to the HolderLookup.Provider, which provides the all registries entries (static and datapack), while createWithoutContext does not provide any datapack access. Vanilla only uses createWithContext. Once the ValueOutput has been used, the CompoundTag can be retrieved via TagValueOutput#buildResult. A TagValueInput, on the other hand, can be created via create, taking in the HolderLookup.Provider and the CompoundTag the input is accessing.

Both value I/Os also take in a ProblemReporter. The ProblemReporter is used to collect all internal errors during the read/write process. Currently, this only tracks Codec errors. How the errors are handled is up to the modder. Vanilla implementations throw if the ProblemReporter is not empty.

// Assume we have access to a HolderLookup.Provider lookupProvider

TagValueOutput output = TagValueOutput.createWithContext(
ProblemReporter.DISCARDING, // Choose to discard all errors
lookupProvider
);

// Write to the output...

CompoundTag tag = output.buildResult();

// Collect the errors
ProblemReporter.Collector reporter = new ProblemReporter.Collector(
// Optionally takes in the root path element
// Some objects (e.g., block entities, entities) have a #problemPath() method that can be supplied
new RootFieldPathElement("example_object")
);

TagValueInput input = TagValueInput.create(
reporter,
lookupProvider,
tag
);

// Read from the input...