CrystalJson reference
The lookup tables for everyday work: the source generator setup, the attributes you put on a type, the settings you pass to a call, and the build diagnostics you might hit. For the task guides see Working with CrystalJson; for the design see What it is and why. Where a behavior changed between releases, the 7.4.2 to 7.4.3 migration guide carries the full story, and this page links to it rather than repeating it.
Every example uses using SnowBank.Data.Json;.
Setup
CrystalJson's runtime types (JsonValue, CrystalJson, CrystalJsonSettings) live in
SnowBank.Core. A project that references SnowBank.Core serializes, parses and uses the DOM
through the reflection path with no further setup.
Generated converters and typed proxies need the source generator as well. It ships as a separate
package that the compiler runs as a Roslyn analyzer. It is a build-time tool, not part of your
shipped application; SnowBank.Core is the only runtime dependency:
<!-- runtime: the JsonValue DOM and the CrystalJson API -->
<PackageReference Include="SnowBank.Core" Version="7.4.6" />
<!-- build-time only: the source generator, a Roslyn analyzer, not redistributed with your app -->
<PackageReference Include="SnowBank.Serialization.Json.CodeGen" Version="7.4.6" />
Give both the same version as your other SnowBank packages, or omit the version under central package
management. The generator requires C# 9 or later: below that it reports SYSLIB1221 and emits
nothing, JSON included, so set <LangVersion> to 9 or higher (the common trigger is a ported project
still pinning a .NET Framework era <LangVersion>7.3</LangVersion>).
A container is a partial class that declares which types it serializes. The generated members land
inside it:
[CrystalJsonConverter]
[CrystalSerializable(typeof(Book))]
public static partial class AcmeSerializers { }
string json = AcmeSerializers.Book.ToJsonText(book);
Book back = AcmeSerializers.Book.Deserialize(json);
Container attributes
The container declares three independent things: which class hosts the code, which formats it produces, and which types it registers.
| Attribute | Namespace | Role |
|---|---|---|
[CrystalConverter] |
SnowBank.Data |
the container marker; names no format on its own |
[CrystalSerializable(typeof(T))] |
SnowBank.Data |
registers a type; repeatable; feeds every format the container produces |
[CrystalJsonOutput(...)] |
SnowBank.Data.Json |
requests the JSON format and carries its parameters (profile, naming policy) |
[CrystalJsonConverter(...)] |
SnowBank.Data.Json |
alias: [CrystalConverter] + [CrystalJsonOutput] with the same parameters, for a JSON-only container |
[CrystalJsonSelfSerializable] |
SnowBank.Data.Json |
meta-attribute for self-serializable types (a type acts as its own container); see the migration guide |
A profile passed to [CrystalJsonOutput(...)] or [CrystalJsonConverter(...)] sets the container's
default output form, CrystalJsonSerializerDefaults.Web for camelCase, .DataContractCompat for the
legacy DataContractJsonSerializer output. Settings passed at a call site replace the profile for
that call.
[CrystalSerializable] replaces the obsolete [CrystalJsonSerializable]; registration is
format-neutral now. XML output has its own attributes and its own page,
CrystalXml.
Member attributes
Put these on a property or field to change how that one member serializes. All are honored on both the reflection path and the generated path.
| Attribute | Effect |
|---|---|
[JsonProperty("name")] |
renames the member on the output |
[JsonProperty(DefaultValue = ...)] |
declares the member's default, used by the WhenWritingDefault ignore condition |
[JsonProperty(EnumFormat = JsonEnumFormat.Number)] |
writes this enum as its number instead of its name |
[JsonProperty(NumberFormat = JsonNumberFormat.String)] |
writes this number as a string ("12345678901234567"), which protects 64-bit values from JavaScript precision loss |
[JsonBooleanLiterals(whenFalse, whenTrue)] |
custom literals for a boolean; a null false literal omits the member when false. Arguments are a string, a bool, or a number |
[JsonIgnore] |
excludes the member (unconditional) |
[JsonIgnore(Condition = ...)] |
conditional exclusion; see the table below |
[JsonConvertWith(typeof(X))] |
serializes the member through the converter X (implements IJsonPacker<T> and/or IJsonDeserializer<T>) |
[JsonInclude] |
includes a non-public member on a type that has no [DataContract] |
[IgnoreDataMember] |
excludes the member on a type that has no [DataContract] |
[JsonIgnore(Condition = ...)] reads JsonIgnoreCondition, following the System.Text.Json meaning.
Note the naming trap: Never means "never ignore".
| Condition | Effect |
|---|---|
Always (the default) |
member excluded |
Never |
member always emitted, overriding the settings-level null and default discards |
WhenWritingNull |
omitted only when the value is null |
WhenWritingDefault |
omitted only when the value equals the member default |
For [DataContract] types, [DataMember(Name = ...)] renames and [DataMember(IsRequired = true)]
makes an absent member throw on read. Generated containers apply the DataContract membership model as
of 7.4.3; the migration guide has the details.
Attributes from other serializers
CrystalJson reads the attributes an existing DTO already carries from System.Text.Json and Newtonsoft.Json (JSON.NET), so a ported type serializes without re-annotation, the same on both paths:
| Foreign attribute | CrystalJson treats it as |
|---|---|
System.Text.Json [JsonPropertyName("x")] |
a rename, like [JsonProperty("x")] |
Newtonsoft [JsonProperty("x")] |
a rename |
[JsonIgnore], either spelling |
exclude the member |
System.Text.Json [JsonIgnore(Condition = ...)] |
conditional exclusion, the conditions above |
System.Text.Json [JsonInclude] |
include a non-public member |
[JsonConverter(typeof(X))], either spelling |
run X, when it implements IJsonPacker<T> and/or IJsonDeserializer<T> |
[DataContract] / [DataMember] |
the DataContract membership model |
When several naming attributes agree, the effective name comes from the highest priority one:
CrystalJson [JsonProperty], then [JsonPropertyName], then Newtonsoft [JsonProperty]. Two naming
attributes that disagree are a build error (CJSON0011): one type cannot serve two output contracts.
A [DataMember] on a [DataContract] type counts as one of them even when it is bare, because it names
the member after itself: a [JsonProperty] that renames the same member is the same conflict.
A foreign [JsonConverter] naming a type that does not implement the CrystalJson converter contract is
ignored, not an error, so a half-ported DTO stays serializable. The
migration guide has the full interop rules.
Settings
Pass a CrystalJsonSettings to a Serialize, Parse, or Deserialize call. Start from a preset
and add fluent modifiers; each modifier returns a new cached instance.
Presets:
| Preset | Output |
|---|---|
CrystalJsonSettings.Json |
the default: readable JSON |
CrystalJsonSettings.JsonCompact |
no whitespace |
CrystalJsonSettings.JsonIndented |
multi-line, indented |
CrystalJsonSettings.JsonStrict |
rejects comments and trailing commas on read |
CrystalJsonSettings.JsonReadOnly |
parses to frozen values |
CrystalJsonSettings.DataContractCompat |
reproduces the DataContractJsonSerializer output |
Common modifiers:
| Modifier | Effect |
|---|---|
.ThrowOnDuplicateFields() |
a repeated key is an error on read, not last-wins |
.WithoutComments() |
reject JavaScript comments on read |
.WithoutTrailingCommas() |
reject a trailing comma on read |
.WithEnumAsNumbers() / .WithEnumAsStrings() |
write enums as their number, or their name (the default) |
.WithNullMembers() / .WithoutNullMembers() |
emit or omit members whose value is null |
.WithoutDefaultValues() |
omit members that equal their default |
.WithMicrosoftDates() / .WithIso8601Dates() |
date format on the output |
.WithIso8601Durations() / .WithNumericDurations() |
TimeSpan as "P1DT2H3M4S", or as a number of seconds (the default) |
.WithDictionariesAsPairArrays() / .WithDictionariesAsMaps() |
a dictionary as an array of {"Key":..,"Value":..}, or as a JSON object map (the default) |
JsonStrict does not cover duplicate fields; add .ThrowOnDuplicateFields() when a repeated key must
fail. To harden the parser for untrusted input, see
Harden parsing for untrusted input.
To read several consecutive documents out of one buffer, use CrystalJson.ParseFragment, not
WithTrailingData() (which parses the first value and drops the rest).
Defaults
- Enums serialize as their name, not their number. Reading accepts names (case-insensitive), numbers, and numeric strings regardless of settings.
- The parser is permissive by default: JavaScript comments and trailing commas are accepted. This
is wrong for input you do not control; tighten it with
JsonStrict. - Numbers keep their source literal on the DOM route until you read them as a typed value.
Diagnostics
The CJSON#### codes below are the ones a normal author hits while writing DTOs. Each is reported at
the same place by both paths: the generator emits the diagnostic, and the reflection path throws the
same message when it builds the type's contract. The
migration guide has the full treatment of each.
| Id | Severity | Rejects | Remedy |
|---|---|---|---|
CJSON0008 |
Error | an unconditional [JsonIgnore] next to an include signal ([DataMember], [JsonInclude], a naming attribute) |
split into one DTO per format, or remove one of the two attributes |
CJSON0010 |
Error | [JsonConvertWith] names a type that implements neither IJsonPacker<T> nor IJsonDeserializer<T> |
implement a converter facet, or fix the named type |
CJSON0011 |
Error | a member declares two different names for two serializers | one DTO per format, each with one coherent set of attributes |
CJSON0012 |
Warning | an internal member with no include or exclude signal, serialized by the generator but invisible to the reflection path |
add [JsonInclude] or [JsonIgnore] to pin the intent |
CJSON0013 |
Error | the DataContractCompat profile combined with a naming policy |
drop the naming policy; the profile fixes the names |
CJSON0015 |
Error | a serialization callback that takes a StreamingContext |
remove the parameter, or replace it with JsonValue, JsonObject, or JsonArray |
CJSON0016 |
Error | [OnDeserializing] on a type with a required or init-only member |
drop [OnDeserializing], or make the member settable |
CJSON0017 |
Error | a [JsonBooleanLiterals] argument that is not a string, bool, or number |
use a valid literal |
CJSON0018 |
Warning | StrictLiterals = true with a null false literal (nothing to enforce on the false side) |
remove StrictLiterals, or give the member a real false literal |
CJSON0019 |
Warning | a [CrystalSerializable] registration of a type CrystalJson already serializes natively |
remove the registration |
CJSON0022 |
Error | [DataMember] on an explicit interface implementation: it belongs to the contract, and generated code cannot declare an accessor for a qualified member name |
promote it to a normal member, or move the contract onto its own DTO |
CJSON0027 |
Error | a type with no parameterless constructor whose constructor cannot be chosen: two public constructors match the members and none carries [JsonConstructor], [JsonConstructor] is on two of them, or a constructor parameter matches no serialized member |
mark one constructor with [JsonConstructor], or align the parameter name with a member |
The self-serializable diagnostics (CJSON0004 to CJSON0007, CJSON0020, CJSON0021) and the XML
generator codes (CRYS####, CXML####) are covered in the
migration guide and CrystalXml.