Structured Output

Copperlace has two rendering modes:

  • Text rendering returns one string.

  • Structured rendering returns an object-shaped value tree.

The same configuration can contain both text rules and structured rules. A top-level rule’s value shape determines how it is rendered:

  • string-valued rules render as text;

  • list-valued rules keep the existing random text choice behavior;

  • map/object-valued rules render structurally;

  • lists inside structured objects render as arrays, not random choices.

Top-level list structured rendering is not supported in v1. Render a top-level list rule as text, or put arrays inside an object-valued structured rule.

Example Configuration

name = ["Mia"]
title = ["moon garden"]

context {
  city = "Istanbul"
  hero = "{name}"
}

origin {
  kind = "scene"
  title = "{title | titlecase}"
  slug = "{title | slug}"
  visitor = "{hero}"
  summary = "{hero} visits {city}."
  tags = ["structured", "{city | slug}"]
  nested {
    greeting = "Hello {hero}."
  }
}

Rendering origin structurally preserves the object and array shape. Text leaves such as title, slug, visitor, summary, and nested.greeting still use normal Copperlace text behavior:

  • rule calls like {title};

  • builtin processors like titlecase and slug;

  • top-level context defaults like {hero} and {city};

  • initial context values passed by the caller or by CLI --set.

Named list rules referenced from text leaves remain random text choices. In the example above, {name} and {title} choose one list item when the text leaf renders. The tags field itself is an array because it is inside a structured object.

Text leaves may contain quoted for blocks, whose body is concatenated into the leaf’s string value. Structured arrays may also contain unquoted for blocks as array entries. Each iteration contributes one typed value to the array, preserving object, array, scalar, and null values rather than building JSON inside a string. See Iteration for the syntax and examples.

Object fields are not an ordering mechanism. If multiple fields need the same generated value, put that value in context defaults or pass it as initial context instead of relying on one sibling field to bind a value before another sibling renders.

Rust Values and JSON

Rust structured APIs return CopperlaceValue internally. CopperlaceValue preserves objects, arrays, strings, numbers, booleans, and null values, and can be serialized to JSON.

CopperlaceValue is the baseline structured output contract for Copperlace:

  • Object preserves object fields as a map keyed by field name. Rust stores fields in BTreeMap, so JSON serialization uses deterministic key order.

  • Array preserves rendered array elements in source and iteration order.

  • String is the rendered text for a structured text leaf. Text leaves use the same rule calls, processors, context defaults, initial context values, and error behavior as text rendering.

  • Number preserves integer, large unsigned integer, and finite floating-point values through CopperlaceNumber.

  • Boolean preserves true and false.

  • Null preserves explicit null values.

The public Rust compile-time tree exposes array entries as StructuredArrayEntry. Fixed entries expose their StructuredNode; template entries expose their loop variable, source path, and body node. This replaces the former Vec<StructuredNode> element type in StructuredNode::Array. Use StructuredArrayEntry::from_value when constructing fixed entries. Callers that inspect or construct this low-level compile-time tree must update to the entry type. CopperlaceValue and the JSON output contract are unchanged.

CopperlaceValue::to_json_value and CopperlaceValue::into_json_value convert the value tree to serde_json::Value without changing its shape. Numbers are converted as JSON numbers: signed integers stay signed, unsigned integers larger than i64::MAX stay unsigned when representable by JSON tooling, and floating point values must be finite.

CopperlaceValue::to_compact_json serializes without extra whitespace. CopperlaceValue::to_formatted_json serializes with tab indentation. Both methods return RenderError::JsonSerialization if JSON serialization fails.

Structured rendering is only supported for object-valued top-level rules. Requesting structured rendering for a string-valued or list-valued top-level rule returns UnsupportedStructuredTarget(rule). Object parents and nested arrays remain valid structured data, but dotted text rendering can return UnsupportedValue("object") or UnsupportedValue("array") for those nodes.

JSON is the public structured serialization target for the C ABI, wrappers, and the CLI. Rust owns the structured value tree; wrapper boundaries intentionally stay render-to-string unless a concrete caller need justifies a richer native value API.

  • C ABI structured functions return JSON strings.

  • Python structured APIs return JSON strings.

  • Java structured APIs return JSON strings.

  • CLI structured output writes JSON to stdout.

CLI Behavior

The CLI does not need a --structured switch. It infers output mode from the selected rule:

  • copperlace render --config story.conf --rule title renders text when title is string-valued or list-valued.

  • copperlace render --config story.conf --rule origin renders structured JSON when origin is object-valued.

Structured CLI JSON is formatted by default using tabs for indentation:

copperlace render --config story.conf --rule origin
{
	"kind": "scene",
	"nested": {
		"greeting": "Hello Mia."
	},
	"slug": "moon-garden",
	"summary": "Mia visits Istanbul.",
	"tags": [
		"structured",
		"istanbul"
	],
	"title": "Moon Garden",
	"visitor": "Mia"
}

Use --compact-json for compact JSON:

copperlace render --config story.conf --rule origin --compact-json
{"kind":"scene","nested":{"greeting":"Hello Mia."},"slug":"moon-garden","summary":"Mia visits Istanbul.","tags":["structured","istanbul"],"title":"Moon Garden","visitor":"Mia"}

--compact-json is valid only for object-valued structured rules. Using it with a text or top-level list rule is an argument error.

Initial context values affect structured text leaves:

copperlace render --config story.conf --rule origin --set city=Kyoto --compact-json

With --count, the CLI emits one JSON value per render. Formatted JSON values are separated by a single newline:

copperlace render --config story.conf --rule origin --count 2

With --count --compact-json, output is newline-delimited compact JSON:

copperlace render --config story.conf --rule origin --count 2 --compact-json

Python API

Python structured APIs are string-based by design. They return JSON strings from Rust instead of reconstructing a Python object tree from serialized output.

from copperlace import Copperlace, render_str_structured

config = '''
name = ["Mia"]
origin {
  title = "Hello {name}"
  tags = ["structured", "{name | slug}"]
  count = 3
  active = true
  missing = null
}
'''

value = render_str_structured(config, "origin")
assert value == '''{
\t"active": true,
\t"count": 3,
\t"missing": null,
\t"tags": [
\t\t"structured",
\t\t"mia"
\t],
\t"title": "Hello Mia"
}'''

with Copperlace.from_string(config) as copperlace:
    value = copperlace.render_structured("origin", {"name": "Lina"})

Python also exposes RuleSet.render_structured, Copperlace.render_structured, render_str_structured, and render_file_structured.

For callers that want the same shape inference as the CLI, Python exposes RuleSet.render_inferred, Copperlace.render_inferred, render_str_inferred, and render_file_inferred. These return normal text for string-valued and list-valued rules, or a formatted JSON string for object-valued rules. Inferred rendering always returns strings.

Java API

Java structured APIs are string-based by design. They return JSON strings from Rust instead of reconstructing a Java object tree from serialized output. This keeps the Java wrapper focused on rendering and avoids adding a second structured-value model outside the Rust core. The no-argument structured JSON overloads return formatted JSON with tabs. Pass false to the overloads with formatJson for compact JSON.

try (Copperlace copperlace = Copperlace.fromString("""
        name = "Mia"
        origin {
          title = "Hello {name}"
          tags = ["structured", "{name | slug}"]
        }
        """)) {
    String formatted = copperlace.renderStructuredJson("origin");
    String compact = copperlace.renderStructuredJson("origin", false);
    String withContext = copperlace.renderStructuredJson(
            "origin",
            Map.of("name", "Lina"),
            true);
    String inferred = copperlace.renderInferred("origin");
}

Java also exposes RuleSet.renderStructuredJson, Copperlace.renderStructuredJson, Copperlace.renderStringStructuredJson, and Copperlace.renderFileStructuredJson. The matching inferred entry points are RuleSet.renderInferred, Copperlace.renderInferred, Copperlace.renderStringInferred, and Copperlace.renderFileInferred; they return text or formatted JSON strings based on the selected rule shape. Copperlace does not expose a Java-native structured value tree; use renderStructuredJson when structured output is needed from Java.