AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified BSD-3-Clause Self-run

Crystaljson

skill-snowbanksdk-foundationdb-dotnet-client-crystaljson · by SnowBankSDK

>-

— No reviews yet
0 installs
28 views
0.0% view→install

Install

$ agentstack add skill-snowbanksdk-foundationdb-dotnet-client-crystaljson

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • ✓ Prompt-injection patterns
  • ✓ Secret / credential exfiltration
  • ✓ Dangerous shell & filesystem operations
  • ✓ Untrusted network calls
  • ✓ Known-malicious package signatures

What it can access

  • ✓ Network access No
  • ✓ Filesystem access No
  • ✓ Shell / process execution No
  • ✓ Environment & secrets No
  • ✓ Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-snowbanksdk-foundationdb-dotnet-client-crystaljson)

Reliability & compatibility

✓ Security review passed
0 installs to date
— no reviews yet
● 17d ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Crystaljson? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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).

  1. 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.

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:

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):

// 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):

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

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.

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).

// 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.

// 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:

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.

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

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.