Serializing Data

While an ImmutableDataManipulator is a good way to store data while the server is running, it will not persist over a restart. However, every DataManipulator implements the DataSerializable interface and thus can be serialized to a DataContainer and deserialized by a DataBuilder.

After this initial conversion from the specialized DataManipulator to a more general structure, the DataContainer can be further processed.

DataContainer and DataView

A DataView is a general-purpose structure for holding any kind of data. It supports multiple values and even nested DataViews as a value, thus allowing for a tree-like structure. Every value is identified by a DataQuery. A DataContainer is a root DataView.

Every DataSerializable provides a toContainer() method which will create and return a DataContainer. As an example, calling toContainer() on a HealthData instance will yield a DataContainer containing two values, one for the current and one for the maximum health, each identified by the DataQuery of the respective Key.

import org.spongepowered.api.data.DataContainer;
import org.spongepowered.api.data.key.Keys;

DataContainer serializedHealth = healthData.toContainer();
double currentHealth = serializedHealth.getDouble(Keys.HEALTH.getQuery()).get();
currentHealth == healthData.health().get();  // true

Converting this container back into a HealthData instance is done by the corresponding DataBuilder. Those are registered and managed by the DataManager. It can either be obtained from a valid Game instance or using the Sponge utility class. The DataManager provides a method to get the appropriate DataBuilder to deserialize a given class and additionally a shorthand method to get the DataBuilder and have it do the deserialization in one step. Both of the following code examples are functionally equivalent.

Code Example: Deserialization, the long way

import org.spongepowered.api.data.DataView;
import org.spongepowered.api.data.manipulator.mutable.entity.HealthData;
import org.spongepowered.api.util.persistence.DataBuilder;

import java.util.Optional;

public Optional<HealthData> deserializeHealth(DataView container) {
    final Optional<DataBuilder<HealthData>> builder = Sponge.getDataManager().getBuilder(HealthData.class);
    if (builder.isPresent()) {
        return builder.get().build(container);
    }
    return Optional.empty();
}

Code Example: Deserialization, the short way

import org.spongepowered.api.data.manipulator.mutable.entity.HealthData;

public Optional<HealthData> deserializeHealth(DataView container) {
    return Sponge.getDataManager().deserialize(HealthData.class, container);
}

The deserializeHealth function will return Optional.empty() if there is no DataBuilder registered for HealthData or the supplied DataContainer is empty. If invalid data is present in the DataContainer, an InvalidDataException will be thrown.

DataTranslator

In Sponge, generally the implementations MemoryDataView and MemoryDataContainer are used, which reside in memory only and thus will not persist over a server restart. In order to persistently store a DataContainer, it first has to be converted into a storable representation.

Using the DataTranslators#CONFIGURATION_NODE implementation of DataTranslator, we can convert a DataView to a ConfigurationNode and vice versa. ConfigurationNodes can then be written to and read from persistent files using the Configurate Library.

Code Example: Serializing a HealthData instance to Configurate

import ninja.leaping.configurate.ConfigurationNode;
import org.spongepowered.api.data.persistence.DataTranslator;
import org.spongepowered.api.data.persistence.DataTranslators;

public ConfigurationNode translateToConfig(HealthData data) {
    final DataTranslator<ConfigurationNode> translator = DataTranslators.CONFIGURATION_NODE;
    final DataView container = data.toContainer();
    return translator.translate(container);
}

Code Example: Deserializing a HealthData instance from Configurate

import java.util.Optional;

public Optional<HealthData> translateFromConfig(ConfigurationNode node) {
    final DataTranslator<ConfigurationNode> translator = DataTranslators.CONFIGURATION_NODE;
    final DataView container = translator.translate(node);
    return deserializeHealth(container);
}

DataFormat

An alternative to using a DataTranslator is to use DataFormat, which allows you to store a DataContainer in HOCON, JSON or NBT format. You can also recreate DataContainers using DataFormats. Sponge provided DataFormat implementations are available in the DataFormats class.

For example, we can use the DataFormats#JSON DataFormat which allows us to create a JSON representation of a DataContainer. The output JSON could then easily be stored in a database. We can then use the same DataFormat to recreate the original DataContainer from this JSON when required.

Imports for code examples

import org.spongepowered.api.Sponge;
import org.spongepowered.api.data.DataContainer;
import org.spongepowered.api.data.persistence.DataFormats;
import org.spongepowered.api.item.inventory.ItemStackSnapshot;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;

Code Example: Serializing an ItemStackSnapshot to JSON format

String json = DataFormats.JSON.write(itemStack.toContainer());

Code Example: Deserializing an ItemStackSnapshot from JSON format

DataContainer container = DataFormats.JSON.read(json);

Code Example: Writing an ItemStackSnapshot to a file using NBT

public void writeItemStackSnapshotToFile(ItemStackSnapshot itemStackSnapshot, Path path) {
    DataContainer dataContainer = itemStackSnapshot.toContainer();
    try (OutputStream outputStream = Files.newOutputStream(path)) {
        DataFormats.NBT.writeTo(outputStream, dataContainer);
    } catch (IOException e) {
        // For the purposes of this example, we just print the error to the console. However,
        // as this exception indicates the file didn't save, you should handle this in a way
        // more suitable for your plugin.
        e.printStackTrace();
    }
}

Code Example: Reading an ItemStackSnapshot from a file using NBT

public Optional<ItemStackSnapshot> readItemStackSnapshotFromFile(Path path) {
    try (InputStream inputStream = Files.newInputStream(path)) {
        DataContainer dataContainer = DataFormats.NBT.readFrom(inputStream);
        return Sponge.getDataManager().deserialize(ItemStackSnapshot.class, dataContainer);
    } catch (IOException e) {
        e.printStackTrace();
    }

    return Optional.empty();
}