| name | system-text-json-newtonsoft-migration |
| version | 10.0.0 |
| description | Use when migrating serialization code from Newtonsoft.Json (Json.NET) to System.Text.Json — replacing JsonConvert / JsonSerializerSettings / [JsonProperty] and reconciling the behavioral differences that compile clean but silently change output (case-sensitivity, fields, nulls, comments, dates). |
Newtonsoft.Json → System.Text.Json migration
Required setup
Remove the Newtonsoft.Json package reference and every using Newtonsoft.Json;. STJ is in the
shared framework — no package reference is needed. The attributes are in a different namespace
from the serializer, which is the usual post-migration compile error:
using System.Text.Json;
using System.Text.Json.Serialization;
Then apply the API map and re-check the behavioral defaults, because most breaks are silent
(compile-clean, wrong output), not compiler errors. The single most common regression: STJ matches
property names case-sensitively, so camelCase JSON into PascalCase members yields null/default
with no exception.
Inventory public fields before changing calls. Newtonsoft serializes them; STJ silently drops
them unless IncludeFields = true or [JsonInclude] is applied. A migrated object unexpectedly
becoming {} is the signature of this miss. Preserve the supplied type shape; do not convert its
fields into properties just to make serialization work.
API map
| Newtonsoft.Json | System.Text.Json |
|---|
JsonConvert.SerializeObject(x) | JsonSerializer.Serialize(x) |
JsonConvert.DeserializeObject<T>(s) | JsonSerializer.Deserialize<T>(s) |
JsonSerializerSettings | JsonSerializerOptions |
[JsonProperty("name")] | [JsonPropertyName("name")] (record param: [property: JsonPropertyName("name")]) |
[JsonIgnore] | [JsonIgnore] (namespace System.Text.Json.Serialization) |
[JsonConstructor] | [JsonConstructor] |
NullValueHandling.Ignore | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull |
DefaultValueHandling.Ignore | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault |
Formatting.Indented | WriteIndented = true |
Required.Always on [JsonProperty] | [JsonRequired] or C# required |
ReferenceLoopHandling.Ignore | ReferenceHandler.IgnoreCycles |
PreserveReferencesHandling.All | ReferenceHandler.Preserve |
StringEnumConverter | JsonStringEnumConverter |
JObject / JArray / JToken | JsonNode / JsonObject / JsonArray (namespace System.Text.Json.Nodes) |
[JsonExtensionData] | [JsonExtensionData] (dictionary must be or ) |
Behavioral differences to reconcile (the silent ones)
- Case-sensitivity. Newtonsoft reads property names case-insensitively; STJ is case-sensitive by
default → set
PropertyNameCaseInsensitive = true (or JsonSerializerDefaults.Web). This is the
single most common migration regression.
- Fields. Newtonsoft serializes public fields; STJ ignores them →
IncludeFields = true /
[JsonInclude].
- Non-public accessors. Newtonsoft uses them; STJ ignores them →
[JsonInclude] or make public.
- Comments / trailing commas. Newtonsoft allows; STJ throws →
ReadCommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true.
- Dates. No
DateFormatString. STJ uses ISO 8601 round-trip; other formats need a custom
JsonConverter<DateTime> (custom converters are covered separately). TimeSpan also differs.
- Numbers. Newtonsoft is lenient about quoted numbers; STJ rejects
"123" into an int unless
NumberHandling = JsonNumberHandling.AllowReadingFromString.
- Missing vs null. STJ leaves a member at its default when the JSON omits it (no
MissingMemberHandling).
Migration checklist
- Swap package + usings + the API-map calls above.
- Inventory public fields and preserve them with
IncludeFields / [JsonInclude].
- Set
PropertyNameCaseInsensitive (or Web defaults) if any input is camelCase.
- Re-add comment/trailing-comma tolerance and quoted-number handling only if the
old code relied on them — don't loosen defaults blindly.
- Replace
StringEnumConverter with JsonStringEnumConverter; replace date-format strings with a converter.
- Build and run a round-trip test — the breaks are silent, so a compile is not enough.