# 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):
| `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.