ytsaurus-yson 0.2.0

YSON serializer and deserializer for YTsaurus (text and binary). Fork of ss123she/yson-rs @ ba2044c
Documentation
# ytsaurus-yson

A [YSON](https://ytsaurus.tech/docs/en/user-guide/storage/yson) serializer and
deserializer (text and binary) built on serde, for talking to
[YTsaurus](https://ytsaurus.tech).

> **This crate is a fork of [ss123she/yson-rs]https://github.com/ss123she/yson-rs**,
> vendored at revision
> [`ba2044c711cefa65259e25122fea21c36f451093`]https://github.com/ss123she/yson-rs/commit/ba2044c711cefa65259e25122fea21c36f451093
> (2026-04-01, published upstream as v0.1.3). Nearly all of the code is upstream's
> work. Everything this fork changed is listed in [CHANGELOG.md]CHANGELOG.md.
>
> The fork exists so that the job runtime can evolve the parser (streaming reads,
> in particular) without waiting on upstream. It is **not** published to crates.io,
> and the `yson-rs` name is not ours to claim.

Licensed under [Apache-2.0](../../LICENSE).

Upstream offers yson-rs under *either* MIT *or* Apache-2.0; this fork takes it
under Apache-2.0, which that dual offer permits. The upstream licence texts are
retained here as received — see [NOTICE](NOTICE) for the full picture.

## Usage

```rust
use serde::{Deserialize, Serialize};
use ytsaurus_yson::{YsonFormat, from_slice, to_vec};

#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct Row {
    key: String,
    count: u64,
}

let row = Row { key: "hello".into(), count: 42 };
let bytes = to_vec(&row, YsonFormat::Binary)?;
let decoded: Row = from_slice(&bytes, YsonFormat::Binary)?;
assert_eq!(row, decoded);
# Ok::<(), ytsaurus_yson::YsonError>(())
```

Attributes map to `@`-prefixed fields, and `$value` holds the body of an
attributed scalar:

```rust
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct ControlRecord {
    #[serde(rename = "@table_index")]
    table_index: i64,
}
// YSON: <table_index=1>#
```

For columns holding arbitrary bytes rather than text, use
[`serde_bytes`](https://docs.rs/serde_bytes) — see the note below.

## Binary format reference

The markers this crate implements, from the
[YSON spec](https://ytsaurus.tech/docs/en/user-guide/storage/yson):

| Marker | Type | Payload |
| --- | --- | --- |
| `0x01` | string | zigzag varint length (`sint32`), then that many raw bytes |
| `0x02` | int64 | zigzag varint (`sint64`) |
| `0x03` | double | 8 bytes, little-endian |
| `0x04` / `0x05` | boolean | false / true |
| `0x06` | uint64 | unsigned varint |
| `0x23` `#` | entity ||
| `0x3C` `0x3E` `< >` | attributes | |
| `0x5B` `0x5D` `[ ]` | list | |
| `0x7B` `0x7D` `{ }` | map | |
| `0x3D` `=` | key/value separator | |
| `0x3B` `;` | item separator | |

## Testing

```sh
cargo test -p ytsaurus-yson      # 78 tests
cargo bench -p ytsaurus-yson     # see BENCHMARKS.md
```

Beyond upstream's suite, this fork adds golden-byte tests for every YTsaurus
control record, round trips against fixtures generated by the **Go** YSON
implementation, and a deterministic no-panic sweep. Performance baseline:
[BENCHMARKS.md](BENCHMARKS.md).

The libFuzzer targets in [`fuzz/`](fuzz/) need nightly and a separate install:

```sh
cargo install cargo-fuzz
cargo +nightly fuzz run fuzz_target_1 -- -max_total_time=60   # binary
cargo +nightly fuzz run fuzz_target_2 -- -max_total_time=60   # text
```

`tests/fuzz_smoke_tests.rs` covers the same entry points deterministically so that
CI still gets a no-panic signal without them.

## Known limitations

Things worth knowing before relying on this crate. None of them are known to be
wrong for the YTsaurus job protocol, but several would bite outside it.

1. **The API takes a whole slice.** `from_slice` needs the entire document in
   memory. Streaming a job's input — which can be far larger than RAM — is the
   job of [`ytsaurus-job`]../ytsaurus-job/, which reparses at record boundaries.

2. **Maps do not preserve key order.** `YsonNode::Map` is a `BTreeMap`, so
   decoding and re-encoding sorts the keys. Values round-trip exactly; bytes do
   not. YTsaurus does not attach meaning to column order, so this is safe for
   rows, but it means you cannot use this type for byte-exact pass-through — use
   the raw-bytes path in `ytsaurus-job` for that.

3. **`String` is preferred over bytes when decoding.** A YSON string that happens
   to be valid UTF-8 is offered to serde as a `str`; only invalid UTF-8 is offered
   as bytes. Decoding into `String` therefore fails on non-UTF-8 columns. For byte
   columns declare the field as `#[serde(with = "serde_bytes")] Vec<u8>`, which
   accepts both. `YsonValue` always keeps the raw bytes.

4. **Text-mode doubles lose precision.** Serialising to text goes through a
   shortest-representation formatter, and YTsaurus documents this as lossy in
   general. Use binary for anything numeric you care about — which is what jobs
   use anyway.

5. **Recursion is capped at depth 128**, and the limit is not configurable.
   Deeper documents fail with `Recursion limit exceeded` rather than overflowing
   the stack. Table rows never come close.

6. **`YsonValue`'s `Index` impl panics** on a missing key or a non-map value.
   Prefer `attr()` / `as_str()` / `as_i64()`, which return `Option`.

7. **Attribute keys collide with `@`-prefixed field names.** Attributes are
   surfaced as fields named `@name`, so a map that genuinely contains the key
   `@name` is indistinguishable from an attribute called `name`. YTsaurus does not
   produce such column names.

8. **No `i128`/`u128` support.** YSON has no such type; values outside
   `int64`/`uint64` are rejected at serialisation time.