Models: parsing and breaking down data

Pipelines often need to work with structured data — a webhook payload, an API response, a file. Flovello represents that structured data as a model: a named schema you define (an object with fields, or an enum), which flows through your pipeline as a single typed value. This tutorial covers the handful of nodes that create, split apart, and serialise models.

What's a model?

A model is a schema you create under Models in the app — a named set of typed fields, which can nest objects and arrays. Here's the Fields editor on an "Order" model's page once defined, with a customerId, a list of line items, and a total:

Fields Add field
customerIdString Non-nullable, required
itemsArray Non-nullable, default empty
Item typeObject
Array item fields Add

nameString Non-nullable, required
quantityInteger Non-nullable, default valueValue

totalDecimal Non-nullable, required
noteString Nullable, default null

Once defined, a model behaves like any other data type: pins can carry a single model value between nodes, typed to that specific model. The rest of this tutorial uses this "Order" model as its running example.

What happens when a field is missing

The dropdown beside each field's type is the one decision that shapes how the rest of your pipeline has to be built. It answers two questions at once, which is why every option names both.

Nullable or non-nullable — is null a value this field accepts at all? A non-nullable field is a promise: it can never hold null, so nothing downstream has to defend against one.

Required, or what it defaults to — what happens when the incoming data doesn't supply the field?

  • Required — parsing fails with an error naming the field. For data you cannot proceed without, like customerId above.
  • Default null — the field becomes null. Only available on a nullable field, since filling null while refusing null is a contradiction.
  • Default empty — for arrays and objects: an empty list, or an object whose own fields are filled by their own rules, all the way down. An items array that defaults to empty means the loop over it runs zero times instead of needing a guard.
  • Default value — for text, numbers, booleans and enum references: the literal you enter. quantity above defaults to 1, so arithmetic downstream never has to cope with a missing number.

The nullability half matters because omitting a key and explicitly sending null are two different messages from whoever is calling you:

{ } (key absent) { "quantity": null }
Non-nullable, default 1 1 1 — null isn't accepted, so the default answers
Nullable, default 1 1 null — the sender meant it, so it's kept
Nullable, required fails null
Non-nullable, required fails fails

Choosing non-nullable is what buys the guarantee. Reach for nullable when data is genuinely, meaningfully absent — every pin carrying a nullable value is marked with a ? in the editor, at both ends of every connection, so you can see exactly how far a possible null travels, and Is null? and Default if null are there to handle it.

The default also applies when you build a value with Make rather than parse one. A Make pin you leave untouched shows what the model will fill in greyed text — null, [], 1 — instead of a value it isn't going to produce. Click it to set something else, or clear it to go back to the model's default. A required field is the one case Make can't fill for you, so leaving its pin empty raises a warning on the node.

Parsing raw input into a model

Raw input — a request body, a file, an API response — usually arrives as a stream of bytes, not a model. The Parse to model node bridges that gap: pick a model in the inspector and a wire format (JSON, YAML, XML, CSV, or Excel), and it parses the stream into a typed value of that model. Because parsing can fail on malformed input, Parse to model sits on the execution flow with two branches — success and error — so you can handle bad input explicitly. When the input is a list rather than a single object, an array variant parses the stream into an array of the chosen model instead.

Breaking a model into fields

Once you have a model value, Break splits it apart. Pick the model in the inspector and the node grows one output pin per field, each typed to match — a string field becomes a string pin, a nested model field becomes another model pin you can Break again. Break is pure: it has no execution pins, and its outputs are computed lazily, only when something downstream reads them.

Composing a model from fields

Make is Break's counterpart: pick a model in the inspector and it grows one input pin per field, then emits the assembled model on its value output. Make is also pure — the model is assembled lazily, only when something reads it. Use it whenever a downstream node expects a whole model rather than loose fields, such as before building a request body.

Serialising a model back out

To turn a model back into text or bytes, use Model to string or Model to stream. Both are pure. Model to stream supports JSON, YAML, XML, CSV, and Excel; Model to string supports all but Excel, because an .xlsx file is a zip archive rather than text. Both take a single model or a list of them — a list becomes a JSON array, an XML container element, or the rows of a CSV file or workbook. Reach for Model to string when you just need the text — for example to log a model with Log message. Reach for Model to stream when the destination is a streaming consumer, such as the body input of an HTTP request — it writes the serialised model straight to the socket without buffering the whole payload as a string first.

Spreadsheets and CSV: mapping a tree onto a grid

A model is a tree; a spreadsheet is a grid. The four other formats can carry a tree directly, so they need almost no configuration. CSV and Excel cannot, so how a model projects onto rows and columns is declared on the model itself, under Hints — model-level settings on the Hints tab, per-field settings behind the lightbulb on each schema row.

The projection has four cases, one per shape a field can have:

Field Becomes
A scalar One column, named by the field's external name
A nested object or model reference Its own fields as columns, sharing a prefix — address.city by default
A list of scalars One column, split on an item separator you choose
A list of models Rows of its own

That last case is the interesting one, and it is what a real order file needs. A list of models can get its rows two ways:

  • From another sheet (Excel only). Give the field a sheet name and a key column, and each parent is matched to the child rows carrying the same key. An Orders sheet and an OrderLines sheet joined on orderNo is the classic supplier workbook.
  • From this same table, grouped by a key column. A flat export that repeats the order columns on every line row is one model per order, not per row. Give the field a parent key column and nothing else, and rows sharing that value collapse into one parent carrying one item each. This is the only option for CSV, which is a single table by definition.

A few things worth knowing before you point a pipeline at a supplier's file:

  • A header row is always required. Without one there is nothing to map a column onto a field by. If a sender really has no header, put a header line in front of the data first.
  • A blank cell means absent, not empty. A table cannot tell the two apart, so a blank cell lets the field's declared default apply — the same as a column the sender left out entirely.
  • The separator and character encoding of a CSV file are detected unless you declare them. A semicolon-separated, ISO-8859-1 export out of a Windows tool is read correctly without configuration.
  • Cells are read by the field's declared type. A STRING field gets what the cell displays, so a date column reads as a date rather than as the serial number underneath it; an INTEGER or DECIMAL field gets what the cell stores, so a price is never rounded to whatever its display format shows. Writing works the same way in reverse, which is why an article number like 0012345 in a STRING field keeps its leading zeros.
  • Only .xlsx and .xlsm are readable. The legacy binary .xls is a different format entirely and has to be re-saved first.

Worked example: reading fields from an order webhook

Here's what it looks like to receive an order webhook, parse its body into an "Order" model, and break that model apart into individual fields:

Receive HTTP
Method (HTTP method)
Headers (HTTP headers)
Body (Input stream)
Parse to Order
Success
Input (Input stream)Model (Order)
Format (Data format)JSONError
Break Order
Value (Order)customerId
items
total

From there, wire customerId into a Log message node. You don't need to wire anything to the Error branch: an unwired execution output already fails the pipeline automatically, so malformed webhooks are surfaced by default. Only wire a Fail node there if you want a more specific failure message than the default.

See also