# iceberg.json

> A module of iceberg's API.

A small, dependency-free JSON reader/writer for Iceberg metadata.

Why not [EmberJson](https://github.com/bgreni/EmberJson)? It resolves fine on
both pixi environments (`emberjson 0.3.4` from `modular-community`), but the
package ships a *precompiled* `emberjson.mojoc` built with Mojo `1.0.0`, and the
nightly compiler hard-errors on it:

    error: Mojo precompiled file is incompatible with the current version of the
    Mojo compiler. Precompiled file '.../emberjson.mojoc' version 1.0.0 is older
    than compiler version 1.1.0.dev2026082905.

Since this tin must build on *both* `default` (nightly) and `stable`, the parser
lives here instead. It is deliberately small: Iceberg metadata JSON is at most a
few hundred KB.

Design: an **arena DOM**. Every value is a `JsonNode` in `Json.nodes`, and
containers refer to their children by index. Recursive Mojo data types are
awkward; integer indices are not.

Numbers are the one place correctness really bites. Snapshot ids, sequence
numbers and `last-updated-ms` are signed 64-bit and must survive a parse →
serialize round trip *exactly*, so integers are accumulated into `Int64` digit
by digit and are never routed through `Float64`. A number is only treated as a
floating-point value when it actually carries a `.`, an `e` or an `E`.

## Structs

### `JsonNode`

```mojo
struct JsonNode
```

One JSON value. Containers hold child node indices, not child values.

Implements: `Copyable`, `Movable`

Fields:

- `kind: UInt8`
- `b: Bool`
- `i: Int64`
- `f: Float64`
- `s: String`
- `keys: List[String]`: Object member names, parallel to `children`. Empty for arrays.
- `children: List[Int]`: Indices into `Json.nodes` of the array elements / object values.

#### `null`

```mojo
def null() -> Self
```

#### `bool_`

```mojo
def bool_(v: Bool) -> Self
```

#### `int_`

```mojo
def int_(v: Int64) -> Self
```

#### `float_`

```mojo
def float_(v: Float64) -> Self
```

#### `string_`

```mojo
def string_(var v: String) -> Self
```

#### `array_`

```mojo
def array_(var kids: List[Int]) -> Self
```

#### `object_`

```mojo
def object_(var keys: List[String], var kids: List[Int]) -> Self
```

### `Json`

```mojo
struct Json
```

A parsed JSON document: an arena of nodes plus the index of the root.

Implements: `Copyable`, `Movable`

Fields:

- `nodes: List[JsonNode]`
- `root: Int`

#### `__init__`

```mojo
def __init__(out self)
```

#### `add`

```mojo
def add(mut self, var node: JsonNode) -> Int
```

#### `kind`

```mojo
def kind(self, i: Int) -> UInt8
```

#### `is_null`

```mojo
def is_null(self, i: Int) -> Bool
```

#### `size`

```mojo
def size(self, i: Int) -> Int
```

Number of elements (array) or members (object).

#### `at`

```mojo
def at(self, i: Int, k: Int) -> Int
```

Index of array element / object value `k`.

#### `key_at`

```mojo
def key_at(self, i: Int, k: Int) -> String
```

#### `get`

```mojo
def get(self, i: Int, key: String) -> Int
```

Index of object member `key`, or -1 when absent.

#### `has`

```mojo
def has(self, i: Int, key: String) -> Bool
```

#### `as_int`

```mojo
def as_int(self, i: Int) -> Int64
```

#### `as_float`

```mojo
def as_float(self, i: Int) -> Float64
```

#### `as_string`

```mojo
def as_string(self, i: Int) -> String
```

#### `as_bool`

```mojo
def as_bool(self, i: Int) -> Bool
```

#### `opt_int`

```mojo
def opt_int(self, i: Int, key: String, dflt: Int64) -> Int64
```

#### `req_int`

```mojo
def req_int(self, i: Int, key: String) -> Int64
```

#### `opt_string`

```mojo
def opt_string(self, i: Int, key: String, dflt: String) -> String
```

#### `req_string`

```mojo
def req_string(self, i: Int, key: String) -> String
```

#### `opt_bool`

```mojo
def opt_bool(self, i: Int, key: String, dflt: Bool) -> Bool
```

#### `string_map`

```mojo
def string_map(self, i: Int, key: String) -> Dict[String, String]
```

Read an object of string→string (Iceberg `properties` and friends).

#### `int_list`

```mojo
def int_list(self, i: Int, key: String) -> List[Int]
```

#### `string_list`

```mojo
def string_list(self, i: Int, key: String) -> List[String]
```

#### `dump`

```mojo
def dump(self, i: Int) -> String
```

Compact JSON text for the subtree rooted at `i`.

#### `dump_root`

```mojo
def dump_root(self) -> String
```

## Functions

### `write_json_string`

```mojo
def write_json_string(s: String, mut out: String)
```

Append `s` as a quoted, escaped JSON string.

### `json_quote`

```mojo
def json_quote(s: String) -> String
```

### `format_double`

```mojo
def format_double(v: Float64) -> String
```

Render a double the way Iceberg's JSON writers do.

`-0.0` keeps its sign (it is a distinct bound value), and non-finite values
become the strings Jackson emits, since JSON has no literal for them.

### `parse_int64`

```mojo
def parse_int64(s: String) -> Int64
```

Exact decimal → Int64, including `-9223372036854775808`.

### `encode_utf8`

```mojo
def encode_utf8(cp: Int, mut out: List[UInt8])
```

### `parse_json`

```mojo
def parse_json(text: String) -> Json
```

Parse a complete JSON document.

### `substr`

```mojo
def substr(s: String, start: Int, end: Int) -> String
```

Byte-range substring. Mojo's `String` has no slice syntax.

## Aliases

### `JSON_NULL`

```mojo
comptime JSON_NULL = UInt8(0)
```

### `JSON_BOOL`

```mojo
comptime JSON_BOOL = UInt8(1)
```

### `JSON_INT`

```mojo
comptime JSON_INT = UInt8(2)
```

### `JSON_FLOAT`

```mojo
comptime JSON_FLOAT = UInt8(3)
```

### `JSON_STRING`

```mojo
comptime JSON_STRING = UInt8(4)
```

### `JSON_ARRAY`

```mojo
comptime JSON_ARRAY = UInt8(5)
```

### `JSON_OBJECT`

```mojo
comptime JSON_OBJECT = UInt8(6)
```
