LinKit.Json.Runtime
1.2.1
dotnet add package LinKit.Json.Runtime --version 1.2.1
NuGet\Install-Package LinKit.Json.Runtime -Version 1.2.1
<PackageReference Include="LinKit.Json.Runtime" Version="1.2.1" />
<PackageVersion Include="LinKit.Json.Runtime" Version="1.2.1" />
<PackageReference Include="LinKit.Json.Runtime" />
paket add LinKit.Json.Runtime --version 1.2.1
#r "nuget: LinKit.Json.Runtime, 1.2.1"
#:package LinKit.Json.Runtime@1.2.1
#addin nuget:?package=LinKit.Json.Runtime&version=1.2.1
#tool nuget:?package=LinKit.Json.Runtime&version=1.2.1
LinKit.Json.Runtime
LinKit.Json.Runtime is a .NET library for JSON serialization, deserialization, and single-value path queries. It provides a convenient System.Text.Json-based API, interoperability with Newtonsoft.Json JToken, optional SpanJson helpers, and metadata-based APIs for applications using source generation or Native AOT.
The library targets .NET 8 and .NET 9.
Contents
- Installation
- Choosing an API
- JConvert
- Source-generated metadata and Native AOT
- JPath
- Newtonsoft.Json integration
- SpanJson integration
- Canonical JSON
- Behavior and limitations
- Build and smoke tests
Installation
Install the NuGet package:
dotnet add package LinKit.Json.Runtime
Or build the library from the repository:
dotnet build LinKit.Json.Runtime\LinKit.Json.Runtime.csproj
The package references Newtonsoft.Json and SpanJson for its integration APIs. System.Text.Json is provided by the target .NET runtime.
Choosing an API
| Scenario | Recommended API |
|---|---|
| General-purpose serialization in a regular JIT application | ToJson, FromJson<T>, FromBytes<T> |
| Native AOT or trimming with generated/static System.Text.Json metadata | *WithTypeInfo APIs |
| UTF-8 serialization/deserialization with System.Text.Json | ToUtf8JsonBytesWithTypeInfo, FromBytesWithTypeInfo or FromBytes<T> |
| Query an in-memory System.Text.Json document | JPath extensions for JsonNode, JsonElement, or JsonDocument |
| Query JSON text without retaining a parsed document | JPath extensions for string |
| Query a Newtonsoft JSON tree | JPath extensions for JToken, or Newtonsoft's SelectTokens for JSONPath operators |
| Use SpanJson directly | ToJsonUtf16, ToJsonUtf8Bytes, FromJsonUtf16<T>, FromJsonUtf8<T> |
The *WithTypeInfo methods use exactly the JsonTypeInfo<T> supplied by the caller. They do not apply JConvertOptions or initialize JConvert's reflection-based compatibility pipeline.
JConvert
Import the namespace:
using LinKit.Json.Runtime;
Serialize and deserialize
var customer = new Customer(42, "Ada Lovelace");
string json = customer.ToJson();
Customer? copy = json.FromJson<Customer>();
public sealed record Customer(int Id, string Name);
ToJson serializes using the object's runtime type. FromJson<T> and FromBytes<T> deserialize to the requested type; FromBytes<T> accepts UTF-8 JSON bytes. Empty or whitespace-only JSON strings, and empty byte arrays, return default; malformed non-empty JSON can throw JsonException.
Configure serialization
ToJson accepts an optional action that configures a JConvertOptions instance for that call:
string json = customer.ToJson(options =>
{
options.NamingConvention = NamingConvention.CamelCase;
options.WriteIndented = true;
options.EnumSerialization = EnumSerializationMode.AsString;
options.LoopHandling = ReferenceLoopHandling.Ignore;
});
Available naming conventions are:
| Value | Result example |
|---|---|
NamingConvention.Unchanged |
CustomerName |
NamingConvention.PascalCase |
CustomerName |
NamingConvention.CamelCase |
customerName |
NamingConvention.SnakeCaseLower |
customer_name |
NamingConvention.KebabCaseLower |
customer-name |
Unchanged is the default. PascalCase uppercases the first character; it does not apply a general word-boundary transformation.
Enum serialization defaults to EnumSerializationMode.AsNumber. Set it to AsString to serialize enum values as their names. The flexible enum converter used by the general-purpose APIs accepts enum names and integer values when deserializing.
Reference loop handling is configured with ReferenceLoopHandling:
Throw(default): do not configure a reference handler; cyclic graphs fail during serialization.Ignore: use System.Text.Json's ignore-cycles behavior.Preserve: use System.Text.Json reference metadata such as$idand$ref.
Preserve represents reference identity in JSON. It does not expand a cyclic graph indefinitely.
Configure System.Text.Json options
FromJson<T> and FromBytes<T> accept a callback that configures JsonSerializerOptions:
Customer? customer = json.FromJson<Customer>(options =>
{
options.PropertyNameCaseInsensitive = true;
options.NumberHandling = System.Text.Json.Serialization.JsonNumberHandling
.AllowReadingFromString;
});
For ToJson, advanced System.Text.Json settings and converters are available through JConvertOptions.SystemTextJsonOptions:
string json = customer.ToJson(options =>
{
options.SystemTextJsonOptions.PropertyNamingPolicy =
System.Text.Json.JsonNamingPolicy.CamelCase;
options.SystemTextJsonOptions.Converters.Add(new CustomerIdConverter());
});
The configured custom converters are applied before the library's default converters. Use the high-level properties on JConvertOptions for naming convention, indentation, enum serialization, and reference loop behavior.
When serializing a Newtonsoft JToken, JConvert uses Newtonsoft.Json for that operation. SystemTextJsonOptions and the optional JsonSerializerContext are not used by that special path.
Untyped object values
The general-purpose deserialization APIs register ObjectToInferredTypesConverter. Values declared as object are materialized recursively:
- JSON objects become
Dictionary<string, object?>. - JSON arrays become
List<object?>. - Strings, booleans, and null retain their corresponding CLR representations.
- Integral JSON numbers that fit in
Int64becomelong; other numbers are read asdouble.
For example:
var payload = """{"name":"Ada","scores":[10,20],"active":true}"""
.FromJson<Dictionary<string, object?>>();
var name = (string?)payload!["name"];
var scores = (List<object?>)payload["scores"]!;
The inferred representation is convenient for dynamic data, but it is not a substitute for a DTO when the JSON schema is known.
Source-generated metadata and Native AOT
For trimming and Native AOT, generate System.Text.Json metadata at build time and pass the matching JsonTypeInfo<T> to a *WithTypeInfo method:
using System.Text.Json.Serialization;
using LinKit.Json.Runtime;
var person = new Person("Ada", 36);
var personTypeInfo = AppJsonContext.Default.Person;
string json = person.ToJsonWithTypeInfo(personTypeInfo);
Person? copy = json.FromJsonWithTypeInfo(personTypeInfo);
byte[] utf8 = person.ToUtf8JsonBytesWithTypeInfo(personTypeInfo);
Person? fromBytes = utf8.FromBytesWithTypeInfo(personTypeInfo);
public sealed record Person(string Name, int Age);
[JsonSerializable(typeof(Person))]
internal partial class AppJsonContext : JsonSerializerContext
{
}
The same metadata can be used when converting a value found by JPath:
using System.Text.Json;
using LinKit.Json.Runtime;
using var document = JsonDocument.Parse(
"""{"profile":{"name":"Ada","age":36}}"""
);
if (document.TryGetValueByPathWithTypeInfo(
"profile",
out Person? profile,
AppJsonContext.Default.Person))
{
Console.WriteLine(profile!.Name);
}
Use [JsonSourceGenerationOptions] on the context to set metadata-generation defaults such as property naming and case sensitivity. The metadata supplied by the caller controls the serialization contract. *WithTypeInfo methods do not merge in JConvertOptions, JConvert's converters, or a reflection fallback.
The regular ToJson, FromJson<T>, FromBytes<T>, and SerializeCanonical APIs remain compatibility APIs and may require reflection or dynamic code. They are annotated for trimming/AOT analysis. Passing a JsonSerializerContext to the general-purpose ToJson or FromJson<T> overload does not make those overloads the recommended Native AOT path.
The Newtonsoft.Json and SpanJson integrations are separate compatibility paths. Do not assume those integrations are Native AOT-safe merely because the System.Text.Json *WithTypeInfo path is.
JPath
JPath selects a single JSON value using property and array-index segments. It supports:
- Dot-separated properties:
profile.name - Array indexes:
items[0] - Negative indexes from the end:
items[-1] - A root prefix:
$.profile.name - Quoted property names containing dots, spaces, quotes, or other special characters:
$['settings.theme'],$["user's name"] - Escaped characters in quoted property names, including backslash, quote, and
\uXXXXescapes
It does not implement wildcard selection, filters, recursive descent, or multi-result queries. Use Newtonsoft.Json's SelectTokens when those JSONPath features are required.
Select a node or element
using System.Text.Json.Nodes;
using LinKit.Json.Runtime;
JsonNode? root = JsonNode.Parse(
"""{"users":[{"name":"Ada"},{"name":"Grace"}]}"""
);
JsonNode? firstUser = root.SelectNode("users[0]");
JsonNode? lastName = root.SelectNode("$.users[-1].name");
SelectNode returns null both when the path is missing and when the selected JSON value is null. Use TrySelectNode to distinguish these cases:
if (root.TrySelectNode("optional", out JsonNode? optional))
{
// The path exists. optional may be null when the JSON value is null.
}
else
{
// The path does not exist.
}
For JsonElement, SelectElement returns null when the path is missing. A selected JSON null is represented by a JsonElement whose ValueKind is JsonValueKind.Null.
using var document = JsonDocument.Parse(
"""{"settings.theme":"dark","values":[10,20,30]}"""
);
JsonElement? theme = document.RootElement.SelectElement("$['settings.theme']");
JsonElement? lastValue = document.SelectElement("values[-1]");
The JsonElement returned from JsonDocument or its root element is backed by that document. Keep the document alive while using the element. SelectElement called on a JSON string parses and clones the selected element, so that result is independent of the temporary document.
Convert a selected value
TryGetValueByPath<T> returns false when the path is missing or the value cannot be converted. GetValueByPath<T> returns the value or throws JsonException if selection or conversion fails.
if (root.TryGetValueByPath<string>("users[0].name", out var name))
{
Console.WriteLine(name);
}
string firstName = root.GetValueByPath<string>("users[0].name");
TryGetValueByPath<T> treats JSON null as a successful result when T can accept null (for example, a reference type or nullable value type). It returns false for JSON null when T is a non-nullable value type.
For parsed JSON scalar values, JPath supports direct conversion for common primitive types and exact numeric conversions. Integral numeric conversions require an integral value and must fit the target type. Complex values are deserialized using System.Text.Json and the optional JsonSerializerOptions.
Query JSON text directly
For a single lookup, query JSON text directly:
string json = """{"orders":[{"id":101},{"id":102}]}""";
if (json.TryGetValueByPath<int>("orders[-1].id", out int orderId))
{
Console.WriteLine(orderId); // 102
}
For multiple queries, parse once and reuse a JsonDocument to avoid parsing the same text repeatedly:
using var document = JsonDocument.Parse(json);
int firstId = document.GetValueByPath<int>("orders[0].id");
int lastId = document.GetValueByPath<int>("orders[-1].id");
String-based TryGetValueByPath returns false for empty or whitespace-only JSON input. Invalid, non-empty JSON is not silently accepted and can throw JsonException.
Newtonsoft.Json integration
JConvert recognizes a Newtonsoft JToken passed to ToJson and serializes it using Newtonsoft.Json. For path queries, use the token-specific selection methods:
using LinKit.Json.Runtime;
using Newtonsoft.Json.Linq;
JToken token = JToken.Parse(
"""{"settings.theme":"dark","items":[{"id":1},{"id":2}]}"""
);
JToken? theme = token.SelectJToken("$['settings.theme']");
if (token.TryGetValueByPath<int>("items[-1].id", out int id))
{
Console.WriteLine(id); // 2
}
TrySelectJToken provides an explicit found/not-found result. The typed TryGetValueByPath<T> overload without JsonTypeInfo<T> uses Newtonsoft.Json's ToObject<T>() conversion behavior and may use runtime metadata.
For source-generated metadata, use the JToken overload that accepts JsonTypeInfo<T>:
bool found = token.TryGetValueByPathWithTypeInfo(
"items[0]",
out Item? item,
AppJsonContext.Default.Item);
This metadata overload serializes the selected token to JSON text and deserializes it with the provided System.Text.Json metadata. It does not use Newtonsoft's ToObject<T>() conversion semantics.
JPath's shared property/index syntax is intentionally a subset of JSONPath. For Newtonsoft-specific query operators or multiple results, use JToken.SelectTokens(path) directly.
SpanJson integration
The SpanJson helpers use the library's default SpanJsonResolver<TSymbol>:
using LinKit.Json.Runtime;
var message = new Message("ready", 3);
string jsonUtf16 = message.ToJsonUtf16();
byte[] spanJsonUtf8 = message.ToJsonUtf8Bytes();
Message fromUtf16 = jsonUtf16.FromJsonUtf16<Message>();
Message fromUtf8 = spanJsonUtf8.FromJsonUtf8<Message>();
public sealed record Message(string State, int Attempt);
These methods use SpanJson's resolver and options, not System.Text.Json's JsonTypeInfo<T> or JConvertOptions. Validate SpanJson support separately for trimming or Native AOT if your application requires it.
Canonical JSON
JConvert provides canonical serialization entry points:
string canonical = value.SerializeCanonical();
byte[] canonicalUtf8 = value.SerializeCanonicalToBytes();
An IJsonTypeInfoResolver can be supplied:
string canonical = value.SerializeCanonical(AppJsonContext.Default);
Canonical output recursively sorts object properties by ordinal UTF-16 order, removes insignificant whitespace, uses ECMAScript-style IEEE 754 number formatting, and rejects duplicate object property names and non-finite numbers. Decimal and integer JSON values are interpreted as IEEE 754 doubles during canonicalization; values outside that numeric model may lose precision, so do not use this API when exact arbitrary-precision numbers must be preserved. The parameterless resolver path uses DefaultJsonTypeInfoResolver and is reflection-based. Validate custom converters and resolver behavior against the RFC 8785 test vectors before relying on the output for signatures or hashes.
Behavior and limitations
- The library targets
net8.0andnet9.0. - General-purpose JConvert APIs use System.Text.Json for .NET types and Newtonsoft.Json for the
JTokenserialization special case. - JConvert's general-purpose deserialization APIs install library converters for enum handling,
objectinference, JsonNode, JToken, and SpanJson dynamic objects. JsonTypeInfo<T>APIs use caller-provided metadata directly. Configure naming, converters, and other serializer behavior on that metadata or its source-generation context.- JPath supports one property/index path and one selected value. It is not a full JSONPath query engine.
- JPath's numeric conversion behavior is designed to avoid silent fractional or out-of-range integer conversions.
- Serialization-option caching reduces repeated configuration work; custom per-call options may require a copy and additional metadata setup. This documentation makes no benchmark-based throughput guarantees.
JConvertOptions.SystemTextJsonOptionsis used by the general-purposeToJsonAPI. Deserialization options are configured through the callback accepted byFromJson<T>orFromBytes<T>.
Build and smoke tests
Build the library:
dotnet build LinKit.Json.Runtime\LinKit.Json.Runtime.csproj -c Release
Run the compatibility smoke application:
dotnet run --project CompatibilitySmokeTest\CompatibilitySmokeTest.csproj -c Release
Run the Native AOT smoke application on Windows x64:
dotnet publish AotSmokeTest\AotSmokeTest.csproj -c Release -f net8.0 -r win-x64
dotnet publish AotSmokeTest\AotSmokeTest.csproj -c Release -f net9.0 -r win-x64
The smoke applications cover representative API behavior; they are not a substitute for application-specific tests or performance benchmarks.
License
The project declares the MIT license in its package metadata. See the repository for the license text and contribution information.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- Newtonsoft.Json (>= 13.0.3)
- SpanJson (>= 4.2.1)
-
net9.0
- Newtonsoft.Json (>= 13.0.3)
- SpanJson (>= 4.2.1)
NuGet packages (3)
Showing the top 3 NuGet packages that depend on LinKit.Json.Runtime:
| Package | Downloads |
|---|---|
|
O24Platform
Nền tảng thư viện dùng chung cho các dự án O24. |
|
|
O24OpenAPI.Framework
Package Description |
|
|
O24OpenAPI.Core
Package Description |
GitHub repositories
This package is not used by any popular GitHub repositories.
LinKit.Json.Runtime 1.2.1
- Improved JPath selection and typed conversion across supported JSON sources.
- Added source-generated metadata APIs for trimming and Native AOT scenarios.
- Improved canonical JSON output and custom System.Text.Json option handling.
- Targets .NET 8 and .NET 9.