Formats and conversion
Convert between JSON, DUST, EDN and TRON with the library's parse and serialize operations, through the C ABI or any binding. Serialization writes a document as text, or as binary bytes for TRON. The example parses the people document and serializes it in the same format.
Example
Serialize the people document
people_text holds this document:
{:#_ada {:name "Ada"
:role "engineer"
:age 36}
:#_bob {:name "Bob"
:role "engineer"
:age 41
:mentor {:#link "#_ada"}}
:#_cy {:name "Cy"
:role "designer"
:age 29}
:#_tx {:author "smoke"
:reason "seed"}}stardust_document *people = NULL;
stardust_text *output = NULL;
stardust_document *result = NULL;
uint32_t status = stardust_parse(people_text, STARDUST_EDN, &people);
if (status == STARDUST_OK)
status = stardust_serialize(people, STARDUST_EDN, 0, &output);
if (status == STARDUST_OK)
status = stardust_parse(stardust_text_data(output), STARDUST_EDN, &result);
stardust_text_release(output);
stardust_document_release(people);
if (status != STARDUST_OK) return status;{:value {:#_ada {:name "Ada"
:role "engineer"
:age 36}
:#_bob {:name "Bob"
:role "engineer"
:age 41
:mentor {:#link "#_ada"}}
:#_cy {:name "Cy"
:role "designer"
:age 29}
:#_tx {:author "smoke"
:reason "seed"}}}Choosing a format
| Format | Use it to |
|---|---|
| DUST | Write readable document examples and query expressions. |
| JSON | Exchange documents with JSON readers. |
| EDN | Exchange values with Clojure readers, under the EDN mapping rules. |
| TRON | Encode a document as binary bytes. |
Warning
A format can change number spelling, member order, or tagged values. Check the fidelity table before you depend on them.
EDN query variables
EDN symbols such as ?name parse as native strings. Serialization writes a native string such as "?name" as the EDN symbol ?name. The string must start with ?, contain a name, and form one valid EDN symbol token. Other string values remain quoted.
Query variables therefore keep their symbol form through EDN, JSON, DUST and TRON conversions. Clojure users can quote a query map to pass its symbols as data:
(with-open [query (sd/from-value
'{:find [?name ?age]
:where [[?e :name ?name] [?e :age ?age]]
:orderBy [?name]})]
(sd/serialize query :edn))DUST layout
An object on one line, {op a args {t 1} e 1}, separates its members with whitespace. A top-level document, and any object that spans lines, holds exactly one member per line, and the rest of the line is the value of that member:
{name Ada Lovelace
role engineer}This object is {"name":"Ada Lovelace","role":"engineer"}. In a multi-line object, {op a args 1 is the member op with the string a args 1. A line that cannot be one bare value, such as {op a args {t 1}, fails with Members_On_One_Line and the offset of its second key. Start each member on its own line, or keep the whole object on one line. Serialization never writes several members on one line of a multi-line object.
Operations
| Operation | Returns | Summary |
|---|---|---|
| stardust_serialize | Status. text through output | document.serialize(format, width) writes text, or binary bytes for TRON. |
| convert | Unavailable | convert(text, from, to, width) calls parse and serialize, then releases the intermediate document. |
stardust_serialize
uint32_t stardust_serialize(const stardust_document *doc, uint32_t format, int64_t max_width, stardust_text **output);stardust_bytes stardust_text_data(const stardust_text *result);uint32_t stardust_text_error(const stardust_text *result, stardust_diagnostic_view *output);void stardust_text_release(stardust_text *result);Parameters
stardust_serialize
| Parameter | Type | Ownership | Default | Meaning |
|---|---|---|---|---|
doc | const stardust_document * | Borrowed handle | Required | Keep the handle alive until the call returns. |
format | uint32_t | Value | Required | The codec to use for this operation. |
max_width | int64_t | Value | Required | Value passed to the call. |
output | stardust_text ** | Owned output | Required | Receives a library-owned handle. Release it after use. |
stardust_text_data
| Parameter | Type | Ownership | Default | Meaning |
|---|---|---|---|---|
result | const stardust_text * | Borrowed handle | NULL | Keep the handle alive until the call returns. |
stardust_text_error
| Parameter | Type | Ownership | Default | Meaning |
|---|---|---|---|---|
result | const stardust_text * | Borrowed handle | Required | Keep the handle alive until the call returns. |
output | stardust_diagnostic_view * | Caller-owned output | Required | Receives a record. Its pointer fields follow the record's ownership contract. |
stardust_text_release
| Parameter | Type | Ownership | Default | Meaning |
|---|---|---|---|---|
result | stardust_text * | Consumed handle | NULL | Ends ownership. Do not use the handle after this call. |
Errors
| Status | When |
|---|---|
STARDUST_INVALID_ARGUMENT | the format is unknown or the width is negative |
STARDUST_OUT_OF_MEMORY | native memory is exhausted |
Examples
document/serialize
expression_text holds this document:
["+" ?x 1]stardust_document *source = NULL;
stardust_text *result = NULL;
uint32_t status = stardust_parse(expression_text, STARDUST_EDN, &source);
if (status == STARDUST_OK) status = stardust_serialize(source, STARDUST_EDN, 0, &result);
stardust_document_release(source);
if (status != STARDUST_OK) {stardust_text_release(result); return status;}{:value {:text "[\"+\" ?x 1]"}}document/serialize
number_text holds this document:
1stardust_document *source = NULL;
stardust_text *result = NULL;
uint32_t status = stardust_parse(number_text, STARDUST_EDN, &source);
if (status == STARDUST_OK) status = stardust_serialize(source, STARDUST_TRON, 0, &result);
stardust_document_release(source);
if (status != STARDUST_OK) {stardust_text_release(result); return status;}{:value {:text "TRON\u0002\u0001\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0004\u0000\u0000\u0000\u0000\u0000\u0000\u0000"}}document/serialize
name_text holds this document:
"Ada"stardust_document *source = NULL;
stardust_text *result = NULL;
uint32_t status = stardust_parse(name_text, STARDUST_EDN, &source);
if (status == STARDUST_OK) status = stardust_serialize(source, STARDUST_TRON, 0, &result);
stardust_document_release(source);
if (status != STARDUST_OK) {stardust_text_release(result); return status;}{:value {:text "TRON<Ada\u0004\u0000\u0000\u0000\u0000\u0000\u0000\u0000"}}document/serialize
expression_text holds this document:
["+" ?x 1]stardust_document *source = NULL;
stardust_text *result = NULL;
uint32_t status = stardust_parse(expression_text, STARDUST_EDN, &source);
if (status == STARDUST_OK) status = stardust_serialize(source, STARDUST_TRON, 0, &result);
stardust_document_release(source);
if (status != STARDUST_OK) {stardust_text_release(result); return status;}{:value {:text "TRON\u001c+,?x\u0002\u0001\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u000e\u0015\u0000\u0007\u0000\u0003\u0000\u0000\u0000\u0004\u0000\u0000\u0000\u0006\u0000\u0000\u0000\t\u0000\u0000\u0000\u0012\u0000\u0000\u0000\u0000\u0000\u0000\u0000"}}document/serialize
expression_text holds this document:
["+" ?x 1]stardust_document *source = NULL;
stardust_text *result = NULL;
uint32_t status = stardust_parse(expression_text, STARDUST_EDN, &source);
if (status == STARDUST_OK) status = stardust_serialize(source, STARDUST_EDN, 0, &result);
stardust_document_release(source);
if (status != STARDUST_OK) {stardust_text_release(result); return status;}{:value {:text "[\"+\" ?x 1]"}}Also calls: stardust_text_data, stardust_text_error, stardust_text_release
convert
Compose stardust_parse and stardust_serialize.
Examples
convert
duplicate_text holds this document:
"{:a 1 :a 2}"stardust_document *source = NULL;
uint32_t root = 0;
stardust_node_view view = {0};
int64_t written = 0;
uint32_t status = stardust_parse(duplicate_text, STARDUST_EDN, &source);
if (status == STARDUST_OK) status = stardust_document_root(source, &root);
if (status == STARDUST_OK) status = stardust_document_read_many(source, root, 1, &view, &written);
if (status != STARDUST_OK) {stardust_document_release(source); return status;}
stardust_document *failed = NULL;
status = stardust_parse(view.text, STARDUST_EDN, &failed);
stardust_document_release(source);
stardust_diagnostic_view result = {0};
stardust_document_error(failed, &result);{:value {:code "invalid_edn"
:message "invalid EDN (Duplicate_Key) at offset 6"
:path nil
:field nil
:entity nil
:offset 6
:status "invalid_argument"}}convert
instant_text holds this document:
#inst "2026-10-07T12:34:56Z"stardust_document *source = NULL;
stardust_text *result = NULL;
uint32_t status = stardust_parse(instant_text, STARDUST_EDN, &source);
if (status == STARDUST_OK) status = stardust_serialize(source, STARDUST_EDN, 0, &result);
stardust_document_release(source);
if (status != STARDUST_OK) {stardust_text_release(result); return status;}{:value {:text "#inst \"2026-10-07T12:34:56Z\""}}convert
container_text holds this document:
[1.0]stardust_document *source = NULL;
stardust_text *output = NULL;
stardust_document *result = NULL;
uint32_t status = stardust_parse(container_text, STARDUST_EDN, &source);
if (status == STARDUST_OK) status = stardust_serialize(source, STARDUST_TRON, 0, &output);
if (status == STARDUST_OK) status = stardust_parse(stardust_text_data(output), STARDUST_TRON, &result);
stardust_text_release(output);
stardust_document_release(source);
if (status != STARDUST_OK) {stardust_document_release(result); return status;}{:value [1]}convert
order_text holds this document:
{:z 2
:a 1}stardust_document *source = NULL;
stardust_text *output = NULL;
stardust_document *result = NULL;
uint32_t status = stardust_parse(order_text, STARDUST_EDN, &source);
if (status == STARDUST_OK) status = stardust_serialize(source, STARDUST_TRON, 0, &output);
if (status == STARDUST_OK) status = stardust_parse(stardust_text_data(output), STARDUST_TRON, &result);
stardust_text_release(output);
stardust_document_release(source);
if (status != STARDUST_OK) {stardust_document_release(result); return status;}{:value {:a 1
:z 2}}Also calls: stardust_parse, stardust_serialize, stardust_text_data, stardust_text_error, stardust_text_release
Fidelity
Each cell shows the result after parsing and serialization in that format. The notes below the table explain each result.
| Value | JSON | DUST | EDN | TRON |
|---|---|---|---|---|
null_boolean | Round trip | Round trip | Round trip | Round trip |
integer | Round trip | Changed | Changed | Changed |
float | Round trip | Round trip | Round trip | Changed |
non_finite | Refused | Changed | Refused | Refused |
decimal_ratio | Round trip | Round trip | Changed | Changed |
string | Round trip | Round trip | Changed | Round trip |
member_order | Round trip | Round trip | Round trip | Changed |
duplicate_keys | Round trip | Round trip | Refused | Changed |
empty_object | Round trip | Round trip | Round trip | Round trip |
utc | Round trip | Round trip | Changed | Round trip |
link | Round trip | Round trip | Round trip | Round trip |
binary | Round trip | Round trip | Round trip | Changed |
edn_forms | Not applicable | Not applicable | Changed | Not applicable |
Fidelity notes
null_boolean- JSON: Round trip. Null and booleans keep their values.
- DUST: Round trip. Null and booleans keep their values.
- EDN: Round trip. EDN nil maps to null.
- TRON: Round trip. Binary tags preserve null and booleans.
integer- JSON: Round trip. The codec preserves integer spelling.
- DUST: Changed. Leading-zero tokens are strings.
- EDN: Changed. Integer spelling is canonical.
- TRON: Changed. Integers keep their value and use canonical spelling.
float- JSON: Round trip. The codec preserves float spelling.
- DUST: Round trip. The codec preserves float spelling.
- EDN: Round trip. The codec preserves this float spelling.
- TRON: Changed. Integral floats in containers become integers.
non_finite- JSON: Refused. JSON has no non-finite numbers.
- DUST: Changed. NaN is a string.
- EDN: Refused. The parser rejects non-finite numeric values.
- TRON: Refused. The parser rejects non-finite binary values.
decimal_ratio- JSON: Round trip. The codec preserves finite decimal spelling.
- DUST: Round trip. The codec preserves finite decimal spelling.
- EDN: Changed. Ratios reduce to native numbers.
- TRON: Changed. Decimals use
f64precision.
string- JSON: Round trip. Strings require UTF-8.
- DUST: Round trip. Number-like strings require quotes.
- EDN: Changed. A lone surrogate becomes a question mark.
- TRON: Round trip. Decoded strings require UTF-8.
member_order- JSON: Round trip. The codec preserves object order.
- DUST: Round trip. The codec preserves object order.
- EDN: Round trip. The codec preserves map order.
- TRON: Changed. Members use trie order.
duplicate_keys- JSON: Round trip. The codec preserves all duplicate members.
- DUST: Round trip. The codec preserves all duplicate members.
- EDN: Refused. The parser rejects equal map keys.
- TRON: Changed. The codec keeps the last duplicate value.
empty_object- JSON: Round trip. The empty object is braces.
- DUST: Round trip. The empty root object serializes to zero bytes.
- EDN: Round trip. The empty map is braces.
- TRON: Round trip. The empty object has a binary footer.
utc- JSON: Round trip. UTC remains a tagged object.
- DUST: Round trip. UTC uses a tagged object.
- EDN: Changed. Millisecond instants use the EDN inst tag.
- TRON: Round trip. UTC remains a plain map.
link- JSON: Round trip. Links remain tagged objects.
- DUST: Round trip. Links remain tagged objects.
- EDN: Round trip. Links remain maps.
- TRON: Round trip. Links remain maps.
binary- JSON: Round trip. Base64 remains a tagged object.
- DUST: Round trip. Base64 remains a tagged object.
- EDN: Round trip. Base64 remains a map.
- TRON: Changed. Valid padded base64 in a sole member becomes a binary node.
edn_forms- JSON: Not applicable. EDN forms have no JSON syntax.
- DUST: Not applicable. EDN forms have no DUST syntax.
- EDN: Changed. The parser converts keywords, symbols, characters, sets and lists to native values. It omits metadata and discarded forms. It reads only the first form.
- TRON: Not applicable. EDN forms have no TRON syntax.
The host mapping on the Language values and documents page shows how language values become documents without a text format.