Skip to content

ObjectTree API

Horizon uses a powerful data API called the ObjectTree API for parsing and managing hierarchical immutable data structures. OT provides a type-safe way to work with multiple file formats including JSON, YAML, TOML, and PROPERTIES

OT is an immutable, thread-safe API that represents file data as a tree structure. It provides mutli-format support, type-safe conversions, alias support, variable interpolation, custom serialization, multiple reader inputs, and strong error handling.

Basic reading of a file is like so:

// Read JSON data
ObjectTree config = ObjectTree.read()
.format(Format.JSON)
.from(new FileInputStream("config.json"))
.parse();
// Access values with automatic type conversion
String serverName = config.getValue("serverName").asString();
int port = config.getValue("port").asInt();
boolean enabled = config.getValue("enabled").asBoolean();
// Access nested structures
ObjectTree database = config.getTree("database");
String host = database.getValue("host").asString();
// Access arrays
ObjectArray plugins = config.getArray("plugins");
for (int i = 0; i < plugins.size(); i++) {
// Converts each entry in the ObjectArray to a String and prints it
System.out.println(plugins.get(i).asString());
}

When reading from JAR entries:

// Parsing a plugin metadata for example
try (InputStream stream = jarFile.getInputStream(entry)) {
ObjectTree metadata = ObjectTree.read()
.format(Format.YAML)
.from(stream)
.parse();
String pluginName = metadata.getValue("name").asString();
String version = metadata.getValue("version").asString();
}

OT provides methods for type conversion with automatic validation:

ObjectValue value = config.getValue("key");
// Built-in type conversions
// If unable to convert for any reason, it will throw a TypeConversionException
String str = value.asString();
int i = value.asInt();
long l = value.asLong();
double d = value.asDouble();
float f = value.asFloat();
boolean b = value.asBoolean();
BigDecimal bd = value.asBigDecimal();
BigInteger bi = value.asBigInteger();
// Optional variants (returns empty Optional on failure)
Optional<Integer> maybeInt = value.asIntOptional();
Optional<String> maybeStr = value.asStringOptional();

Register custom converters for your own types:

ObjectTree config = ObjectTree.read()
.format(Format.JSON)
// Note: this is for example, both UUID and File are implemented by default
.registerConverter(File.class, obj -> new File(obj.toString()))
.registerConverter(UUID.class, obj -> UUID.fromString(obj.toString()))
.from(inputStream)
.parse();
File pluginDir = config.getValue("pluginsDirectory").as(File.class);
UUID serverId = config.getValue("serverId").as(UUID.class);

For complex objects, register custom deserializers:

// Define your data class
record ServerConfig(String host, int port, boolean ssl) {
}
// Register deserializer
ObjectTree config = ObjectTree.read()
.format(Format.JSON)
.registerDeserializer(ServerConfig.class, tree ->
new ServerConfig(
tree.getValue("host").asString(),
tree.getValue("port").asInt(),
tree.getValueOptional("ssl")
.map(v -> v.asBoolean())
.orElse(false)
)
)
.from(inputStream)
.parse();
// Deserialize directly to your type
ServerConfig server = config.as(ServerConfig.class);

Support multiple key names that map to the same value:

ObjectTree config = ObjectTree.read()
.format(Format.YAML)
.alias("host", "db_host", "databaseHost", "dbHost")
.alias("port", "db_port", "databasePort", "dbPort")
.from(inputStream)
.parse();
// This makes it so your YAML file you are parsing can use `db_host`, or `dbHost` in the
// actual YAML file, but when read it is remapped to `host`, to be fetched like below
String host = config.getValue("host").asString();

OT supports variable substitution using ${variable} syntax:

ObjectTree config = ObjectTree.read()
.format(Format.YAML)
.withVariable("region", "us-west-2")
.from(inputStream)
.parse();
// YAML file with variables:
// server:
// endpoint: https://${region}.example.com
// dataDir: ${env.HOME}/horizon
String endpoint = config.getTree("server").getValue("endpoint").asString();
// Result: "https://us-west-2.example.com"
String dataDir = config.getTree("server").getValue("dataDir").asString();
// Result: "/home/user/horizon" (uses environment variable)

Builtin variable sources:

  • Environment variables: ${env.VARIABLE_NAME}
  • System properties: ${sys.property.name}
  • Custom variables: Added via .withVariable()

Create and save data programmatically:

// Build an example data structure
ObjectTree config = ObjectTree.builder()
.put("serverName", "My Server")
.put("port", 25565)
.build();
// Write as JSON
try (FileWriter writer = new FileWriter("config.json")) {
ObjectTree.write(config)
.format(Format.JSON)
.to(writer);
}
// Or get as string
String yaml = ObjectTree.write(config)
.format(Format.YAML)
.toString();

All formats are interchangeable, you can read data in one format and write it in another. There is also safer access with optionals:

// Get optional values
Optional<ObjectValue> maybeValue = config.getValueOptional("optional-key");
Optional<ObjectTree> maybeTree = config.getTreeOptional("optional-section");
Optional<ObjectArray> maybeArray = config.getArrayOptional("optional-list");
// Chain optionals for safe access
int port = config.getTreeOptional("server")
.flatMap(server -> server.getValueOptional("port"))
.map(v -> v.asInt())
.orElse(25565);