Dictionaries

A Dictionary is an ordered collection of key-value pairs. It is the fundamental data structure for representing structured data in Parsleyโ€”everything from simple configuration objects to database rows, JSON responses, and template contexts.

let person = {name: "Alice", age: 30, city: "London"}
person.name

Result: "Alice"

Key characteristics:


Creating Dictionaries

Basic Syntax

Use curly braces with key: value pairs separated by commas:

{name: "Bob", score: 100}

Result: {name: "Bob", score: 100}

Keys can be unquoted identifiers or quoted strings:

{firstName: "Ada", "last-name": "Lovelace", "with spaces": true}

Result: {firstName: "Ada", "last-name": "Lovelace", "with spaces": true}

Empty Dictionary

{}

Result: {}

Nested Dictionaries

Dictionaries can contain other dictionaries:

{
    user: {name: "Alice", id: 42},
    settings: {theme: "dark", notifications: true},
}

Result: {user: {name: "Alice", id: 42}, settings: {theme: "dark", notifications: true}}

Computed Keys

Use square brackets [expr] for keys determined at runtime:

let field = "email"
{name: "Alice", [field]: "alice@example.com"}

Result: {name: "Alice", email: "alice@example.com"}

Computed keys can use variables determined at runtime:

let keyName = "status"
{id: 1, [keyName]: "active"}

Result: {id: 1, status: "active"}

Function Values and this

Dictionaries can contain functions as values. When a function is called as a method on a dictionary, this is automatically bound to the dictionary:

let user = {name: "Sam", greet: fn() { "Hi, " + this.name }}
user.greet()

Result: "Hi, Sam"

Methods can accept arguments and reference multiple properties:

let calc = {value: 10, add: fn(x) { this.value + x }}
calc.add(5)

Result: 15

Methods can call other methods on the same dictionary:

let p = {name: "Bob", greet: fn() { "Hi " + this.name }, hello: fn() { this.greet() + "!" }}
p.hello()

Result: "Hi Bob!"

Built-in methods (.keys(), .has(), etc.) continue to work alongside user-defined methods.


Accessing Values

Dot Notation

Access values using dot notation when the key is a valid identifier:

let config = {host: "localhost", port: 8080}
config.host

Result: "localhost"

Bracket Notation

Use bracket notation for any key, including those with special characters:

let data = {"my-key": "value", "with spaces": 42}
data["my-key"]

Result: "value"

Bracket notation also allows dynamic key access:

let data = {a: 1, b: 2, c: 3}
let key = "b"
data[key]

Result: 2

Missing Keys

Accessing a missing key returns null:

let d = {name: "Alice"}
d.missing

Result: null

Safe Access with ?

Use optional access [?key] for explicit null-safety (behaves the same as normal access but makes intent clearer):

let d = {a: 1}
d[?"missing"]

Result: null


Operators

++ (Concatenation / Merge)

Merge two dictionaries with the ++ operator. When keys conflict, the right dictionary's values win:

{a: 1, b: 2} ++ {b: 3, c: 4}

Result: {a: 1, b: 3, c: 4}

The result preserves order: keys from the left dictionary come first (in their original order), followed by new keys from the right:

{z: 1, a: 2} ++ {m: 3, a: 99}

Result: {z: 1, a: 99, m: 3}

in (Key Membership)

Test if a key exists using the in operator:

let user = {name: "Alice", role: "admin"}
"name" in user

Result: true

"email" in user

Result: false

The negated form not in is also available:

"email" not in user

Result: true

Note: The in operator checks for keys, not values. To check if a value is in the dictionary, use .values() with array membership.

& (Intersection)

Create a new dictionary containing only keys present in both dictionaries:

{a: 1, b: 2, c: 3} & {b: 20, c: 30, d: 40}

Result: {b: 2, c: 3}

The values come from the left dictionary.

- (Subtraction)

Remove keys present in the right dictionary from the left:

{a: 1, b: 2, c: 3} - {b: 0, c: 0}

Result: {a: 1}

The values in the right dictionary are ignoredโ€”only the keys matter.


Destructuring

Extract values from a dictionary into variables using destructuring patterns.

Basic Destructuring

let {name, age} = {name: "Alice", age: 30, city: "London"}
name

Result: "Alice"

Renaming with as

Rename variables during destructuring:

let {name as userName, age as userAge} = {name: "Bob", age: 25}
userName

Result: "Bob"

Rest Operator

Capture remaining keys with ...rest:

let {id, ...rest} = {id: 1, name: "Alice", active: true}
rest

Result: {name: "Alice", active: true}

Nested Destructuring

Destructure nested dictionaries:

let {user: {name}} = {user: {name: "Alice", id: 1}}
name

Result: "Alice"

Default Values

Missing keys become null:

let {name, missing} = {name: "Alice"}
missing

Result: null


Iteration

For Loops

Iterate over a dictionary with for. The loop provides key and value:

let scores = {alice: 95, bob: 87, carol: 92}
for (name, score in scores) {
    name ++ ": " ++ score
}

Result: ["alice: 95", "bob: 87", "carol: 92"]

Keys are returned in insertion order.

Single Variable Iteration

With a single variable, you get the value:

for (v in {a: 1, b: 2}) { v }

Result: [1, 2]

To iterate over keys only, use .keys():

for (k in {a: 1, b: 2}.keys()) { k }

Result: ["a", "b"]


Methods

as()

Convert a dictionary to a Record by applying a schema:

@schema User {
    name: string(required)
    age: int
}
let data = {name: "Alice", age: 30}
let user = data.as(User)
user.isValid()

Result: false

The .as() method creates an unvalidated Record. Call .validate() on the result to perform validation:

data.as(User).validate().isValid()

Result: true

See also: Records, Schema


reorder()

Reorder and optionally rename dictionary keys. Returns a new dictionary.

With an array argument โ€” select and reorder keys:

let d = {a: 1, b: 2, c: 3, d: 4}
d.reorder(["c", "a"])

Result: {c: 3, a: 1}

Only keys listed in the array are included, in the specified order. Keys not in the array are dropped.

With a dictionary argument โ€” rename and reorder keys:

let user = {first_name: "Alice", last_name: "Smith", age: 30}
user.reorder({name: "first_name", surname: "last_name"})

Result: {name: "Alice", surname: "Smith"}

The dictionary maps new key names to old key names ({newKey: "oldKey"}). Keys are output in the order specified in the mapping. Keys not in the mapping are dropped.

This is useful for:

Errors:


delete()

Remove a key from the dictionary. This is the only method that mutates the original dictionary.

let d = {a: 1, b: 2, c: 3}
d.delete("b")
d

Result: {a: 1, c: 3}

Returns null. The dictionary is modified in place.


entries()

Return an array of {key, value} dictionaries:

{name: "Alice", age: 30}.entries()

Result: [{key: "name", value: "Alice"}, {key: "age", value: 30}]

Customize the key and value names:

{name: "Alice", age: 30}.entries("field", "data")

Result: [{field: "name", data: "Alice"}, {field: "age", data: 30}]

Entries are returned in insertion order.


has()

Check if a key exists:

{name: "Alice", age: 30}.has("name")

Result: true

{name: "Alice"}.has("email")

Result: false


insertAfter()

Insert a new key-value pair after an existing key. Returns a new dictionary:

{a: 1, c: 3}.insertAfter("a", "b", 2)

Result: {a: 1, b: 2, c: 3}

Errors if the existing key doesn't exist or the new key already exists.


insertBefore()

Insert a new key-value pair before an existing key. Returns a new dictionary:

{a: 1, c: 3}.insertBefore("c", "b", 2)

Result: {a: 1, b: 2, c: 3}

Errors if the existing key doesn't exist or the new key already exists.


keys()

Return an array of all keys in insertion order:

{name: "Alice", age: 30, city: "London"}.keys()

Result: ["name", "age", "city"]


render()

Render a template string, substituting @{expr} placeholders with dictionary values:

let person = {name: "Ada", born: 1815}
person.render("@{name} was born in @{born}.")

Result: "Ada was born in 1815."

The content inside @{...} is a full Parsley expression with access to the dictionary's keys:

let data = {price: 100, tax: 0.2}
data.render("Total: @{price * (1 + tax)}")

Result: "Total: 120"


repr()

Return a string representation suitable for debugging. Note that dictionary values are lazily evaluated, so repr() may show <unevaluated> for complex values:

let d = {name: "Alice", count: 3}
d.repr()

Result: "{name: <unevaluated>, count: <unevaluated>}"

For fully evaluated representations, use .toJSON() instead.


toBox()

Render the dictionary as an ASCII box:

{name: "Alice", age: 30}.toBox()

Result:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ name  โ”‚ Alice โ”‚
โ”‚ age   โ”‚ 30    โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Options:

{name: "Alice", age: 30}.toBox({title: "User", style: "rounded"})

Result:

โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
โ”‚     User      โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ name  โ”‚ Alice โ”‚
โ”‚ age   โ”‚ 30    โ”‚
โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ

Available options:


toHTML()

Convert the dictionary to an HTML definition list:

{name: "Alice", role: "Admin"}.toHTML()

Result: "<dl><dt>name</dt><dd>Alice</dd><dt>role</dt><dd>Admin</dd></dl>"


toJSON()

Convert the dictionary to a JSON string:

{name: "Alice", active: true}.toJSON()

Result: "{\"active\":true,\"name\":\"Alice\"}"


toMarkdown()

Convert the dictionary to a Markdown table:

{name: "Alice", age: 30}.toMarkdown()

Result:

| Key | Value |
|-----|-------|
| name | Alice |
| age | 30 |

values()

Return an array of all values in insertion order:

{name: "Alice", age: 30, city: "London"}.values()

Result: ["Alice", 30, "London"]


Order Preservation

Dictionaries in Parsley are ordered. They maintain the order in which keys were inserted. This is different from many programming languages where hash maps/dictionaries are unordered.

let d = {z: 1, a: 2, m: 3}
d.keys()

Result: ["z", "a", "m"]

Order is preserved through:


Relationship to Other Types

Records

A Record is a schema-bound dictionary. It carries type information, validation state, and field metadata. Convert a dictionary to a Record using .as(Schema):

@schema User { name: string, age: int }
let user = {name: "Alice", age: 30}.as(User)

See: Records

Tables

A Table is an array of dictionaries (rows) with consistent column structure. Tables can be created from arrays of dictionaries:

@table [
    {name: "Alice", age: 30}
    {name: "Bob", age: 25}
]

Individual table rows are dictionaries (or Records if the table has a schema).

See: Tables

Special Dictionary Types

Many Parsley features are implemented as dictionaries with special __type markers:

These types have specialized methods and operators but remain dictionaries internally.

HTML Forms

Dictionaries are the natural format for HTML form data. In Basil handlers, props is a dictionary of form field names and values:

// In a Basil handler
export save = fn(props) {
    // props is a dictionary: {name: "Alice", email: "alice@example.com"}
    let form = User(props).validate()
    // ...
}

JSON

Dictionaries map directly to JSON objects. Use .toJSON() to serialize and @fetch responses automatically parse JSON into dictionaries:

let data = {name: "Alice", scores: [95, 87]}
data.toJSON()

Result: "{\"name\":\"Alice\",\"scores\":[95,87]}"

Note: .toJSON() converts Parsley types to JSON-compatible values (dates become strings, money becomes numbers). For lossless serialization of Parsley data, write to PLN instead: data ==> PLN(@./file.pln). See Data Formats.


Spread in HTML Tags

Dictionary values can be spread into HTML tag attributes using ...dict:

let attrs = {class: "button", id: "submit-btn"}
<button ...attrs>Click</button>

Result: <button class="button" id="submit-btn">Click</button>

Later attributes override earlier ones:

let base = {class: "btn", disabled: true}
<button ...base class="btn-primary">Submit</button>

Result: <button disabled class="btn-primary">Submit</button>


Equality

Dictionaries are compared by reference, not by value:

{a: 1} == {a: 1}

Result: false

To compare dictionary contents, compare their JSON representations or check individual keys.


Common Patterns

Building Dictionaries Dynamically

Use computed keys and the merge operator:

let base = {type: "user"}
let extra = {id: 42, name: "Alice"}
base ++ extra

Result: {type: "user", id: 42, name: "Alice"}

Filtering Dictionary Keys

Keep only certain keys using destructuring:

let {name, age, ...drop} = {name: "Alice", age: 30, password: "secret", token: "xyz"}
{name, age}

Result: {name: "Alice", age: 30}

Default Values with Merge

Provide defaults by merging:

let defaults = {theme: "light", fontSize: 14}
let userPrefs = {theme: "dark"}
defaults ++ userPrefs

Result: {theme: "dark", fontSize: 14}

Converting Arrays to Dictionaries

Use for to transform an array into dictionary entries:

let pairs = [["a", 1], ["b", 2], ["c", 3]]
let result = {}
for (pair in pairs) {
    let key = pair[0]
    result = result ++ {[key]: pair[1]}
}
result

Result: {a: 1, b: 2, c: 3}


See Also