# Crystaljson

> >-

- **Type:** Skill
- **Install:** `agentstack add skill-snowbanksdk-foundationdb-dotnet-client-crystaljson`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [SnowBankSDK](https://agentstack.voostack.com/s/snowbanksdk)
- **Installs:** 0
- **Category:** [Databases](https://agentstack.voostack.com/c/databases)
- **Latest version:** 0.1.0
- **License:** BSD-3-Clause
- **Upstream author:** [SnowBankSDK](https://github.com/SnowBankSDK)
- **Source:** https://github.com/SnowBankSDK/foundationdb-dotnet-client/tree/master/plugins/foundationdb-skills/skills/crystaljson
- **Website:** https://snowbanksdk.github.io/foundationdb-dotnet-client/

## Install

```sh
agentstack add skill-snowbanksdk-foundationdb-dotnet-client-crystaljson
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# CrystalJson (SnowBank.Data.Json)

CrystalJson is a high-performance, allocation-conscious JSON stack. It is **not** `System.Text.Json` or Newtonsoft -
the type names look familiar (`JsonObject`, `JsonArray`, ...) but the API is different. The namespace is
`SnowBank.Data.Json`. Add `using SnowBank.Data.Json;`.

There are **two layers**, used together:

1. **The DOM** - `JsonValue` and its subtypes. A mutable-or-immutable tree you build, parse, navigate, and serialize.
   Use it for schemaless / dynamic JSON (config, arbitrary documents, change records).
2. **The source generator** - `[CrystalJsonConverter]` + `[CrystalSerializable(typeof(T))]` generate fast,
   reflection-free, AOT-friendly converters for your POCOs, plus typed **read-only / writable proxies** over the DOM.
   Use it for your domain types.

`CrystalJson` (static class) is the entry point for serialize/parse/deserialize regardless of layer.

---

## 1. The JsonValue DOM

`JsonValue` is the abstract base. Concrete types and their `JsonType`:

| Type | `JsonType` | Notes |
|---|---|---|
| `JsonObject` | `Object` | key -> value map; mutable **or** read-only |
| `JsonArray` | `Array` | ordered list; mutable **or** read-only |
| `JsonString` | `String` | immutable |
| `JsonNumber` | `Number` | immutable; small ints cached |
| `JsonBoolean` | `Boolean` | immutable; only `True`/`False` singletons |
| `JsonDateTime` | `DateTime` | immutable; serialized as an ISO string |
| `JsonNull` | `Null` | three distinct singletons (below) |

**The three nulls - this trips people up:**

- `JsonNull.Null` - an **explicit** null that was present in the JSON (`{"x": null}`).
- `JsonNull.Missing` - a field that **was not there** (`obj["absent"]`) or an out-of-range array read.
- `JsonNull.Error` - result of an **invalid** access (e.g. indexing a non-array).

All three report `value.IsNull == true`. Distinguish them with `value.IsNullOrMissing()`, `value.IsMissing()`,
`value.IsError()`, or `ReferenceEquals(value, JsonNull.Missing)`. Parsing an empty/whitespace/`null` input gives
`JsonNull.Missing`; parsing the literal `"null"` gives `JsonNull.Null`.

Other useful singletons: `JsonBoolean.True/False`, `JsonNumber.Zero/One`, `JsonObject.ReadOnly.Empty`,
`JsonArray.ReadOnly.Empty`.

---

## 2. Read-only vs mutable - the core mental model

This is the most important concept. `JsonObject` and `JsonArray` can each be **mutable** or **read-only**
(`value.IsReadOnly`). Scalars (string/number/bool/null/datetime) are always read-only.

- **Mutating a read-only container throws** `InvalidOperationException` ("Cannot mutate ... because it is marked as
  read-only").
- A read-only value is safe to **cache and share across threads**.
- Conversions:
  - `value.ToReadOnly()` - returns self if already read-only, else a deep read-only copy.
  - `value.ToMutable()` - returns a mutable copy (minimal copying); use before editing a possibly-frozen value.
  - `value.Copy(deep: true, readOnly: false)` - explicit copy.
  - `value.Freeze()` - freezes in place (only on values you exclusively own).

**Build mutable, then optionally freeze; or build read-only directly with the `ReadOnly` factory.**

```csharp
using SnowBank.Data.Json;

// mutable, with the Create factories (the default pattern; implicit conversions cover scalars)
var obj = JsonObject.Create([
    ("name", "Alice"),
    ("age", 30),
    ("tags", JsonArray.Create("admin", "user")),
    ("point", JsonObject.Create([ ("x", 1), ("y", 2) ])),
]);
var arr = JsonArray.Create(1, 2, 3);
// (the collection-initializer form `new JsonObject { ["name"] = "Alice" }` compiles too, but the
// factories read identically in their mutable and ReadOnly forms, so prefer them)

// read-only directly (good for cached/shared constants): the ReadOnly twin, same call shape
var ro = JsonObject.ReadOnly.Create([
    ("name", "Alice"),
    ("age", 30),
    ("tags", JsonArray.ReadOnly.Create(["admin", "user"])),
]);

// from a CLR value (POCO, collection, primitive)
JsonValue v   = JsonValue.FromValue(myPoco);              // mutable
JsonValue rov = JsonValue.ReadOnly.FromValue(myPoco);     // read-only

obj.ToReadOnly();   // freeze for caching
ro.ToMutable();     // get a mutable copy to edit
```

---

## 3. Reading and navigating

Indexers **never throw** on a missing key/index - they return `JsonNull.Missing` (or `JsonNull.Error`), so you can chain
safely:

```csharp
JsonValue city = obj["user"]["address"]["city"];   // Missing if any hop is absent; no NRE
bool present   = !obj["user"].IsNullOrMissing();
```

**Read + convert in one step** (the everyday API):

```csharp
// Get: optional with default, or required (throws JsonBindingException if null/missing/incompatible)
int    age   = obj.Get("age", 0);          // default if absent
string name  = obj.Get("name");         // throws if absent/null
Guid   id    = obj.Get("id");

// TryGet
if (obj.TryGet("email", out var email)) { /* ... */ }

// typed children
JsonObject child = obj.GetObjectOrEmpty("meta");   // never null; empty read-only object if absent
JsonArray  items = obj.GetArray("items");          // throws if not an array
if (obj.TryGetObject("meta", out var meta)) { /* ... */ }

// arrays
int count = items.Count;
string first = items.Get(0);
foreach (var item in items) { /* JsonValue */ }
foreach (var o in items.AsObjects()) { /* JsonObject items only */ }
```

**Convert a `JsonValue` to a CLR type** (when you already hold the value):

```csharp
string? s = jv.As();          // default(T) (null) if the value is null/missing
int     n = jv.As(-1);           // custom default if null/missing
string  r = jv.Required();    // throws if null/missing
```

`As` / `Get` support primitives, `Guid`/`Uuid*`, `DateTime`/`DateTimeOffset`/`DateOnly`/`TimeSpan`,
NodaTime `Instant`/`Duration`, `Uri`, `byte[]`/`Slice`, arrays/`List`, and your POCOs. Numbers/dates use
**InvariantCulture**.

---

## 4. CrystalJson: serialize / parse / deserialize

```csharp
using SnowBank.Data.Json;

// SERIALIZE a CLR value -> JSON
string json   = CrystalJson.Serialize(value);                          // formatted, single line
string compact= CrystalJson.Serialize(value, CrystalJsonSettings.JsonCompact);
string pretty = CrystalJson.Serialize(value, CrystalJsonSettings.JsonIndented);
Slice  bytes  = CrystalJson.ToSlice(value, CrystalJsonSettings.JsonCompact);   // UTF-8
byte[] raw    = CrystalJson.ToBytes(value);
CrystalJson.SerializeTo(textWriterOrStream, value);                    // streaming

// PARSE text/bytes -> DOM: parse through the DOM types, not through CrystalJson.*
JsonValue  any = JsonValue.Parse(json);      // string, Slice, ReadOnlySpan
JsonObject o   = JsonObject.Parse(json);     // throws if it is not an object
JsonArray  a   = JsonArray.Parse(json);      // throws if it is not an array
// READ-ONLY (cache-safe) twin of each: the nested ReadOnly class, same entry points
JsonValue roDom = JsonValue.ReadOnly.Parse(json);   // also JsonObject.ReadOnly.Parse, etc.

// DESERIALIZE text/bytes -> POCO (parse + bind)
Book book  = CrystalJson.Deserialize(json);                      // throws if the JSON is null
Book? maybe= CrystalJson.Deserialize(json, defaultValue: null);  // null instead of throwing

// Serialize a JsonValue back to text/bytes
string s2 = value.ToJsonText();                  // or ToJsonText(settings)
Slice  b2 = value.ToJsonSlice(CrystalJsonSettings.JsonCompact);
```

**Parse (DOM) vs Deserialize (POCO):** `Parse` returns a `JsonValue` tree you navigate; `Deserialize` binds straight
to your type. A `null`/empty/missing input deserializes to `null` -> throws for a non-nullable `T` unless you pass a
`defaultValue`.

The intended split: **`CrystalJson.*` serves the POCO route** (`Serialize`, `Deserialize`, `ToSlice`), **the DOM
parses through the DOM types** (`JsonValue.Parse`, `JsonObject.Parse`, `JsonArray.Parse`, each a `new static`
returning the derived type, throwing when the payload has another shape). Pick `JsonObject.Parse` when a non-object
payload is a bug (let it throw); pick `JsonValue.Parse` plus a type test when it is an ordinary case to handle.
(`CrystalJsonDomWriter.ParseObject(value)` is unrelated: it goes the other way, CLR value -> DOM.)

### CrystalJsonSettings

Settings are **immutable and cached**; start from a preset and compose with fluent methods.

Presets: `CrystalJsonSettings.Json` (default), `.JsonCompact`, `.JsonIndented`, `.JsonReadOnly` (parse a read-only DOM),
`.JsonStrict`, `.JsonIgnoreCase` (case-insensitive field matching), and `JavaScript*` variants.

Common fluent options (chainable, e.g. `CrystalJsonSettings.Json.Compacted().CamelCased()`):

- Layout: `.Compacted()`, `.Indented()`, `.Formatted()`
- Naming: `.CamelCased()`, `.PascalCased()`
- Nulls/defaults: `.WithoutNullMembers()` (default), `.WithNullMembers()`, `.WithoutDefaultValues()`
- Enums: `.WithEnumAsStrings()` (**the default since 7.4.3**), `.WithEnumAsNumbers()` - see *Enums in the output* in section 9 for
  what changed and the recipes that restore numbers
- Dates: `.WithIso8601Dates()` (default), `.WithMicrosoftDates()` (emits `"\/Date(ms)\/"`; **reading** that legacy
  format always works, with or without this setting)
- Durations: `.WithNumericDurations()` (default: `TimeSpan` as a number of seconds), `.WithIso8601Durations()`
  (emits the legacy `"P1DT2H3M4.005S"` duration string; **reading** both forms always works) *(7.4.3+)*
- Dictionaries: `.WithDictionariesAsMaps()` (default, `{"k":v}`), `.WithDictionariesAsPairArrays()` (emits the legacy
  `[{"Key":k,"Value":v}]` shape; again, **reading** both shapes always works) *(7.4.3+)*
- Read-only result: `.AsReadOnly()`

**Parsing leniency** (deserialization only; none of these change what you emit). The parser is deliberately
permissive by default, which is wrong for untrusted input:

| Option | Default | Tighten with | Loosen with |
|---|---|---|---|
| JavaScript comments (`// ...`, `/* ... */`) | accepted | `.WithoutComments()` | `.WithComments()` |
| trailing commas (`[1, 2, ]`) | accepted | `.WithoutTrailingCommas()` | `.WithTrailingCommas()` |
| content after the top-level value | rejected | `.WithoutTrailingData()` | `.WithTrailingData()` |
| duplicate field names | last one wins | `.ThrowOnDuplicateFields()` | `.FlattenDuplicateFields()` |

⚠️ The **property** is `settings.AllowTrailingData`; the fluent **method** that sets it is `.WithTrailingData()`.
There is no `.AllowTrailingData()` method. Same shape for the others: read a `bool` property, set it with a
`With*` / `Without*` method.

`CrystalJsonSettings.JsonStrict` is the shorthand for the first two rows (no comments, no trailing commas). It
does **not** touch duplicate fields, so add `.ThrowOnDuplicateFields()` yourself if a repeated key must be an
error rather than a silent overwrite.

To read several consecutive documents out of one buffer, use `CrystalJson.ParseFragment` or the streaming
reader instead of `.WithTrailingData()`, which parses the first value and silently drops the rest.

---

## 5. The source generator (your domain types)

For POCOs, prefer the generator over the DOM: it emits a fast, reflection-free, AOT-friendly converter **and** typed
read-only/writable proxies. (This is how the document-collection layers built on this stack store their documents.)

### Declare

Put `[CrystalSerializable(typeof(T))]` (one per root type) on a `public static partial class` marked
`[CrystalJsonConverter]`. Nested types are discovered automatically. Use `[JsonProperty("name")]` to rename a field.

```csharp
using SnowBank.Data.Json;

public sealed record Book
{
    [JsonProperty("id")]
    public required string Id { get; init; }

    [JsonProperty("title")]
    public required string Title { get; init; }

    [JsonProperty("year")]
    public int Year { get; init; }
    public Author? Author { get; init; }   // nested type: auto-discovered
}

[CrystalJsonConverter]                       // or [CrystalJsonConverter(CrystalJsonSerializerDefaults.Web)] for camelCase + ignore-case
[CrystalSerializable(typeof(Book))]
public static partial class AcmeSerializers { }       // generated members land here
```

#### The container vocabulary *(7.4.4+)*

`[CrystalJsonConverter]` is a **mono-format alias**: it means "this class hosts generated code" **plus**
"produce the JSON format, with these parameters". The two halves also exist separately, which is what a
container producing several formats needs:

| Attribute | Namespace | Role |
|---|---|---|
| `[CrystalConverter]` | `SnowBank.Data` | the container marker; says nothing about the formats |
| `[CrystalSerializable(typeof(T))]` | `SnowBank.Data` | registers a root type; repeatable; feeds every output format |
| `[CrystalJsonOutput(...)]` | `SnowBank.Data.Json` | requests the JSON format (profile, naming policy, case-insensitivity) |
| `[CrystalXmlOutput(...)]` | `SnowBank.Data.Xml` | requests the XML format (see `Documentation/CrystalXml.md`) |
| `[CrystalJsonConverter(...)]` | `SnowBank.Data.Json` | alias: `[CrystalConverter]` + `[CrystalJsonOutput]`, JSON only |
| `[CrystalXmlConverter(...)]` | `SnowBank.Data.Xml` | alias: `[CrystalConverter]` + `[CrystalXmlOutput]`, XML only |

Rules the compiler enforces: a `[CrystalConverter]` naming no output format is refused (`CRYS0001`);
a mono-format alias next to an output attribute is refused (`CRYS0002` - use `[CrystalConverter]` with
explicit output attributes instead); several container markers on one class are refused (`CRYS0003`).

```csharp
// a container that produces BOTH formats from one set of registered types
[CrystalConverter]
[CrystalJsonOutput(CrystalJsonSerializerDefaults.Web)]
[CrystalXmlOutput]
[CrystalSerializable(typeof(Book))]
public static partial class CatalogSerializers { }
```

`[CrystalJsonSerializable(typeof(T))]` is the former spelling of `[CrystalSerializable]`: still working
and byte-identical, but `[Obsolete]` (registration never was JSON-specific).

### Self-serializable types: the entity IS its own container *(7.4.3+)*

The container above (`AcmeSerializers`) is one way to register a type. The other is to let the type carry its own
generated code, which is what you want when a **layer** owns a vocabulary and should not force every consuming
application to also declare a JSON container.

`[CrystalJsonSelfSerializable]` is a **meta-attribute**: you put it on one of *your own* attribute classes, and
every type decorated with that attribute is opted into generation.

```csharp
// the layer declares its vocabulary ONCE
[CrystalJsonSelfSerializable]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public sealed class MyEntityAttribute : Attribute { }

// the application just declares an entity - no container, no [CrystalSerializable]
[MyEntity]
public sealed partial record Widget
{
    public required string Name { get; init; }
    public Author? Author { get; init; }      // referenced types are still picked up
}
```

Everything generated lands inside **one** nested static class named `Json`, so the entity reserves exactly one
member name:

```csharp
Widget.Json.Default          // the converter (the container mode's AcmeSerializers.Widget)
Widget.Json.ReadOnly         // read-only proxy
Widget.Json.Writable         // writable proxy
Widget.Json.PropertyNames    // property-name constants
Widget.Json.GetResolver()    // the per-container resolver, same as the container mode
Widget.Json.ToJsonText(w)    // the static helpers live there too
```

Referenced types nest inside that same scope under their plain names: `Author`'s converter is
`Widget.Json.Author.Default`. Inside the scope they cannot shadow the referenced type in the entity's own
source, which is what the single reserved name buys.

The one-name rule is the design, not an implementation detail: a future generator for another format claims a
sibling scope (`Widget.Cbor`) without renegotiating anything. It also means the `Json` scope is entirely
generated code, so it carries the genera

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [SnowBankSDK](https://github.com/SnowBankSDK)
- **Source:** [SnowBankSDK/foundationdb-dotnet-client](https://github.com/SnowBankSDK/foundationdb-dotnet-client)
- **License:** BSD-3-Clause
- **Homepage:** https://snowbanksdk.github.io/foundationdb-dotnet-client/

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-snowbanksdk-foundationdb-dotnet-client-crystaljson
- Seller: https://agentstack.voostack.com/s/snowbanksdk
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
