# Maps, lists, sets & nested structures

This is the page that ties everything together. It builds the payloads developers actually move around with Tree — request parameters, list responses, records with a hidden metadata block, descriptor objects — and for every shape it shows the same three steps: build it → show the JSON → read it back. The examples are generic (a request payload, a list of user records, a response with a metadata block) but modelled on real message shapes.

If you need the underlying method reference, see Data Manipulation (write side) and Reading values (read side).

import io.datatree.Tree;
import java.util.Map;
import java.util.LinkedHashMap;
import java.util.List;

Read this gotcha first. putList(path) / putMap(path) / putSet(path) return the new child container, not the document root. Keep a reference to the root (or call getRoot()) when you want to serialize the whole structure:

Tree root = new Tree();
Tree ids  = root.putList("ids");   // 'ids' is the LIST node, not the root
ids.add(101).add(102);
System.out.println(root.toString(true));
// { "ids":[101,102] }

# Flat object (Map) — a request payload

The simplest shape is a flat map of named values — for example the parameters of a service call. Because the keys are flat (no dots), the put calls chain on the root:

Tree params = new Tree()
        .put("firstName", "John")
        .put("lastName", "Doe")
        .put("age", 30)
        .put("active", true);
{
  "firstName":"John",
  "lastName":"Doe",
  "age":30,
  "active":true
}

Read it back with typed getters and null-safe defaults:

String firstName = params.get("firstName").asString();   // "John"
int    age       = params.get("age", 0);                  // 30
String nickname  = params.get("nickname", "n/a");         // "n/a" (absent)

for (Tree field : params) {
    System.out.println(field.getName() + " = " + field.asString());
}

# Array of scalars (List) — a list of ids or tags

Tree root = new Tree();
Tree ids  = root.putList("ids");
ids.add(101).add(102).add(103);
{
  "ids":[
    101,
    102,
    103
  ]
}

Process the elements with a stream, or bulk-convert them to a typed List:

int sum = root.get("ids").stream().mapToInt(Tree::asInteger).sum();   // 306
List<Integer> asList = root.get("ids").asList(Integer.class);         // [101, 102, 103]

# Set — deduplicating values (e.g. roles)

A Set node behaves exactly like a List for the caller — same add API — but it stores no duplicates:

Tree root  = new Tree();
Tree roles = root.putSet("roles");
roles.add("admin").add("user").add("admin");   // "admin" added twice, stored once
{
  "roles":[
    "admin",
    "user"
  ]
}

# Array of objects — the headline case

A list whose elements are maps is the shape you reach for constantly: a list of records. Use addMap() to append a fresh object and fill it:

Tree root  = new Tree();
Tree users = root.putList("users");
users.addMap().put("id", 1).put("name", "Alice");
users.addMap().put("id", 2).put("name", "Bob");
{
  "users":[
    {
      "id":1,
      "name":"Alice"
    },
    {
      "id":2,
      "name":"Bob"
    }
  ]
}

Read it back three ways — iterate, by index, and by path:

// 1) Iterate the records:
for (Tree user : root.get("users")) {
    int    id   = user.get("id", 0);
    String name = user.get("name", "");
    System.out.println(id + " -> " + name);
}
// 1 -> Alice
// 2 -> Bob

// 2) By path, with an array index:
String second = root.get("users[1].name").asString();   // "Bob"

# Object with a nested array + nested object — a descriptor

Real descriptors nest: an array of objects, each of which contains its own arrays. Build one with addMap().putList(...), then read deep values both by nested loops and by path:

Tree root     = new Tree();
Tree services = root.putList("services");
Tree svc      = services.addMap();
svc.put("name", "math");
svc.putList("nodes").add("node-1").add("node-2");
svc.putList("ipList").add("10.0.0.1").add("10.0.0.2");
{
  "services":[
    {
      "name":"math",
      "nodes":[
        "node-1",
        "node-2"
      ],
      "ipList":[
        "10.0.0.1",
        "10.0.0.2"
      ]
    }
  ]
}
// Nested-loop idiom: walk services, then the nodes inside each:
for (Tree serviceInfo : root.get("services")) {
    String name = serviceInfo.get("name", "");
    for (Tree node : serviceInfo.get("nodes")) {
        String nodeID = node.asString();
    }
}

// Or read a deep value directly by path:
String firstIp = root.get("services[0].ipList[0]").asString();   // "10.0.0.1"

# A record + its metadata block (_meta)

A document can carry a metadata node next to its body. This is how a clean response body keeps its status code and headers out of the way: the body holds the data, getMeta() holds the out-of-band fields.

Tree rsp = new Tree();
rsp.put("result", "ok");

Tree meta = rsp.getMeta();
meta.put("$statusCode", 200);
meta.put("$responseType", "application/json");
Tree headers = meta.putMap("$responseHeaders");
headers.put("Content-Type", "application/json; charset=utf-8");

toString() (no arguments) serializes the body with the metadata block, under the _meta key:

{
  "result":"ok",
  "_meta":{
    "$statusCode":200,
    "$responseType":"application/json",
    "$responseHeaders":{
      "Content-Type":"application/json; charset=utf-8"
    }
  }
}

toString(true) serializes the body without the metadata:

{
  "result":"ok"
}

Whether the metadata appears in the output is controlled by the insertMeta flag — see The _meta block.

# From existing Java collections

You can wrap a prepared Java Map/List directly, or drop one into a path. new Tree(map) makes the map the document root:

Map<String, Object> address = new LinkedHashMap<>();
address.put("city", "Phoenix");
address.put("zip", "85001");

Tree root = new Tree(address);
{
  "city":"Phoenix",
  "zip":"85001"
}

putObject(path, object) (or addObject / setObject) places a prepared object under a key:

Tree doc = new Tree();
doc.putObject("address", address);
{
  "address":{
    "city":"Phoenix",
    "zip":"85001"
  }
}

The *Object methods unwrap Tree values for you: if the object you pass is a Tree, or a Collection/array that contains Tree elements, the underlying values are stored (not the Tree wrappers), so the result serializes cleanly.

# Converting between container types

setType(Class) converts a node's container in place. Converting a Map whose keys are "0", "1", … to a List keeps the values in order and drops the keys:

Tree root = new Tree();
Tree data = root.putMap("data");
data.put("0", "a").put("1", "b");

root.get("data").setType(List.class);   // Map -> List
{
  "data":[
    "a",
    "b"
  ]
}

A Set never stores duplicates, so converting a List with repeats to a Set collapses them. With these building blocks — flat maps, scalar lists, deduplicating sets, arrays of objects, deep nesting, and an optional metadata block — you can construct and read any request, response or event shape.

Next: Serialization & I/O turns these structures into JSON (or other formats) and parses them back.