Skip to content

Formats and documents

A format states how bytes are documents: how one document is read from bytes and written to them, and how one document is separated from the next. The formats are the Format types of the writing results out chapter. source and tap decode with them, target and sink encode with them.

What one document reads as

Format One document reads as Documents are separated by
Json Data whitespace between top-level values
Yaml Data a --- line
Csv Data, one row keyed by the header a line; the header is read once
Lines String, one line without its terminator a line terminator
JsonLines Data, one JSON document per line a line terminator
Text, Markdown, Hmd, Hql String, the whole text not separable: one document

Lines and JsonLines are framing formats, one document per line. The same file read as Lines gives its lines as strings, and read as Json or JsonLines the documents those lines hold:

source(file("examples/io/sightings.jsonl"), Lines) | take(1)
["{\"species\": \"barn owl\", \"site\": \"meadow\", \"seen\": \"dusk\"}"]

A whole-text format reads one document, the whole file:

source(file("examples/io/counts.csv"), Text) | size
1

Bytes that do not decode in their format are an evaluation failure naming the endpoint, the line and the column.

D2 and Mermaid describe a whole, so they are written by target only:

source(file("examples/io/sightings.jsonl"), Mermaid)
check error[type-mismatch]: Mermaid is written, not read
 --> <expression>:1:8
  |
1 | source(file("examples/io/sightings.jsonl"), Mermaid)
  |        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
source(file("examples/io/sightings.jsonl")) | sink(stdout, Mermaid)
check error[type-mismatch]: Mermaid describes a whole, so it is written by `target`, not `sink`
 --> <expression>:1:47
  |
1 | source(file("examples/io/sightings.jsonl")) | sink(stdout, Mermaid)
  |                                               ^^^^^^^^^^^^^^^^^^^^^

The extension implies the format

When the format argument is left out, the extension of a literal file path implies it:

Extension Format
.json Json
.jsonl, .ndjson JsonLines
.yaml, .yml Yaml
.csv Csv
.txt, .log Lines
.md Markdown
.hmd Hmd
.hql Hql
.d2 D2
.mmd Mermaid

A format that is given wins over the extension: source(file("notes.data"), Json) and target(file("result.txt"), Json) are both allowed. An extension outside the table implies none, and leaving the format out is then a type error naming the argument:

source(file("examples/io/notes.data"))
check error[type-mismatch]: the extension of "examples/io/notes.data" implies no format; name it: `source(…, Json)`
 --> <expression>:1:8
  |
1 | source(file("examples/io/notes.data"))
  |        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The format is not the shape

A format states the encoding, and the type states the shape. A Seq written by target in Json is one array; written by sink it is one document per line, which Json reads back, since whitespace separates top-level JSON values. sink(e, F) and source(e, F) are inverses: the documents one writes are the documents the other reads, in order.

Reading in one format and writing in another converts a file. A CSV cell is text, so the counts arrive in YAML as strings:

source(file("examples/io/counts.csv")) | target(stdout, Yaml)
- count: '2'
  species: barn owl
- count: '1'
  species: wren
- count: '1'
  species: kingfisher

Read as a declared record, the same cells are the Int the record declares (reading with source).