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
Overview
Section titled “Overview”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 Usage
Section titled “Basic Usage”Reading Data
Section titled “Reading Data”Basic reading of a file is like so:
// Read JSON dataObjectTree config = ObjectTree.read() .format(Format.JSON) .from(new FileInputStream("config.json")) .parse();
// Access values with automatic type conversionString serverName = config.getValue("serverName").asString();int port = config.getValue("port").asInt();boolean enabled = config.getValue("enabled").asBoolean();
// Access nested structuresObjectTree database = config.getTree("database");String host = database.getValue("host").asString();
// Access arraysObjectArray 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());}Reading from JAR Resources
Section titled “Reading from JAR Resources”When reading from JAR entries:
// Parsing a plugin metadata for exampletry (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();}Type Conversions
Section titled “Type Conversions”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 TypeConversionExceptionString 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();Custom Type Converters
Section titled “Custom Type Converters”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);Custom Object Deserialization
Section titled “Custom Object Deserialization”For complex objects, register custom deserializers:
// Define your data classrecord ServerConfig(String host, int port, boolean ssl) {}
// Register deserializerObjectTree 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 typeServerConfig server = config.as(ServerConfig.class);Alias Support
Section titled “Alias Support”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 belowString host = config.getValue("host").asString();Variable Interpolation
Section titled “Variable Interpolation”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()
Writing and Optionals
Section titled “Writing and Optionals”Create and save data programmatically:
// Build an example data structureObjectTree config = ObjectTree.builder() .put("serverName", "My Server") .put("port", 25565) .build();
// Write as JSONtry (FileWriter writer = new FileWriter("config.json")) { ObjectTree.write(config) .format(Format.JSON) .to(writer);}
// Or get as stringString 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 valuesOptional<ObjectValue> maybeValue = config.getValueOptional("optional-key");Optional<ObjectTree> maybeTree = config.getTreeOptional("optional-section");Optional<ObjectArray> maybeArray = config.getArrayOptional("optional-list");
// Chain optionals for safe accessint port = config.getTreeOptional("server") .flatMap(server -> server.getValueOptional("port")) .map(v -> v.asInt()) .orElse(25565);