vm2.Linq.Expressions.Serialization.Json
1.0.0
See the version list below for details.
dotnet add package vm2.Linq.Expressions.Serialization.Json --version 1.0.0
NuGet\Install-Package vm2.Linq.Expressions.Serialization.Json -Version 1.0.0
<PackageReference Include="vm2.Linq.Expressions.Serialization.Json" Version="1.0.0" />
<PackageVersion Include="vm2.Linq.Expressions.Serialization.Json" Version="1.0.0" />
<PackageReference Include="vm2.Linq.Expressions.Serialization.Json" />
paket add vm2.Linq.Expressions.Serialization.Json --version 1.0.0
#r "nuget: vm2.Linq.Expressions.Serialization.Json, 1.0.0"
#:package vm2.Linq.Expressions.Serialization.Json@1.0.0
#addin nuget:?package=vm2.Linq.Expressions.Serialization.Json&version=1.0.0
#tool nuget:?package=vm2.Linq.Expressions.Serialization.Json&version=1.0.0
vm2.Linq.Expressions — LINQ Expression Tree Serialization for .NET
- vm2.Linq.Expressions — LINQ Expression Tree Serialization for .NET
Overview
This repository provides a set of .NET packages for serializing and deserializing LINQ expression trees to and from XML and JSON documents. Expression trees are in-memory representations of code as data — lambda expressions, method calls, member access, conditionals, loops, and more. .NET compiles these into executable delegates. These packages let you persist, transmit, and reconstruct expression trees across process and machine boundaries.
The serialization produces human-readable, schema-backed documents that preserve the full structure of the expression's abstract syntax tree (AST), including types, parameters, constants, and control flow. Both the XML and JSON formats can optionally be validated against their respective schemas.
A companion package provides structural deep-equality comparison and hash code computation for expression trees, useful for caching, deduplication, and testing.
Packages
| Package | Description |
|---|---|
| vm2.Linq.Expressions.Serialization.Xml | Serialize and deserialize expression trees to/from XML (XDocument). |
| vm2.Linq.Expressions.Serialization.Json | Serialize and deserialize expression trees to/from JSON (JsonObject). |
| vm2.Linq.Expressions.Serialization.Abstractions | Shared abstractions, conventions, vocabulary, and extension methods (dependency of Xml and Json). |
| vm2.Linq.Expressions.DeepEquals | Structural deep-equality comparison and hash code computation for expression trees. |
Prerequisites
- .NET 10.0 or later
Install the Packages (NuGet)
Install the serialization package for the format you need. Each serialization package transitively references
Serialization.Abstractions.
Using the dotnet CLI:
dotnet add package vm2.Linq.Expressions.Serialization.Xml dotnet add package vm2.Linq.Expressions.Serialization.Json dotnet add package vm2.Linq.Expressions.DeepEqualsFrom Visual Studio Package Manager Console:
Install-Package vm2.Linq.Expressions.Serialization.Xml Install-Package vm2.Linq.Expressions.Serialization.Json Install-Package vm2.Linq.Expressions.DeepEquals
Quick Start
Each serialization package provides extension methods on Expression and static deserialization methods for the simplest
possible API. For advanced scenarios (reusing a transform instance, custom options), use ExpressionXmlTransform or
ExpressionJsonTransform directly — see the examples directory.
Serialize to XML
using System.Linq.Expressions;
using vm2.Linq.Expressions.Serialization.Xml;
Expression<Func<int, int, int>> addExpr = (a, b) => a + b;
// Expression → XML string
string xml = addExpr.ToXmlString();
// Expression → XML file
addExpr.ToXmlFile("expression.xml");
// Expression → XDocument
XDocument doc = addExpr.ToXmlDocument();
// Round-trip back to Expression
Expression roundTrip = doc.ToExpression();
// Deserialize from file / stream / string
Expression fromFile = ExpressionXml.FromFile("expression.xml");
Expression fromString = ExpressionXml.FromString(xml);
Serialize to JSON
using System.Linq.Expressions;
using vm2.Linq.Expressions.Serialization.Json;
Expression<Func<int, int, int>> addExpr = (a, b) => a + b;
// Expression → JSON string
string json = addExpr.ToJsonString();
// Expression → JSON file
addExpr.ToJsonFile("expression.json");
// Expression → JsonObject
JsonObject doc = addExpr.ToJsonDocument();
// Round-trip back to Expression
Expression roundTrip = doc.ToExpression();
// Deserialize from file / stream / string
Expression fromFile = ExpressionJson.FromFile("expression.json");
Expression fromString = ExpressionJson.FromString(json);
All methods accept an optional XmlOptions or JsonOptions parameter for customization:
addExpr.ToXmlFile("expression.xml", new XmlOptions { Indent = true, IndentSize = 4 });
Stream and writer overloads are also available (sync and async):
using var stream = new MemoryStream();
addExpr.ToXmlStream(stream);
stream.Position = 0;
Expression fromStream = ExpressionXml.FromStream(stream);
Deep Comparison
using System.Linq.Expressions;
using vm2.Linq.Expressions.DeepEquals;
Expression<Func<int, int>> original = x => x * 2;
Expression<Func<int, int>> duplicate = x => x * 2;
Expression<Func<int, int>> different = x => x + 2;
Console.WriteLine(original.DeepEquals(duplicate)); // True
Console.WriteLine(original.DeepEquals(different)); // False
// With difference explanation
if (!original.DeepEquals(different, out string difference))
Console.WriteLine(difference); // describes the first structural difference
// Structural hash code for caching/deduplication
int hash = original.GetDeepHashCode();
Unsupported Expression Types
Both the XML and JSON serializers support virtually all LINQ expression node types — arithmetic, bitwise, logical, comparison, assignment, increment/decrement, type operations, member access, method calls, invocations, object and collection creation, lambdas, parameters, blocks, conditionals, loops, switches, try/catch/finally, gotos, labels, constants of all primitive and complex types, and more.
The following expression types are not supported and will throw NotImplementedExpressionException if encountered:
| Expression Type | Reason |
|---|---|
DebugInfo |
Compiler-generated debugging metadata; not meaningful outside the debugger. |
Dynamic |
DLR dynamic dispatch (C# dynamic); depends on runtime binders that cannot be serialized. |
RuntimeVariables |
Provides runtime access to variable values; intrinsically tied to the execution context. |
Extension |
Custom expression nodes from third-party providers; no universal serialization is possible. |
Configuration Options
Both XmlOptions and JsonOptions extend the shared DocumentOptions base class:
Common Options
| Property | Default | Description |
|---|---|---|
Indent |
true |
Indent the output document for readability. |
IndentSize |
2 |
Number of spaces per indentation level. |
Identifiers |
Preserve |
Naming convention for identifiers: Preserve, Camel, Pascal, SnakeLower, SnakeUpper. |
TypeNames |
FullName |
How types are written: FullName, Name, AssemblyQualifiedName. |
AddComments |
false |
Add explanatory comments to the output document. |
AddLambdaTypes |
false |
Include explicit type annotations on lambda parameters. |
ValidateInputDocuments |
IfSchemaPresent |
When to validate input documents: Never, Always, IfSchemaPresent. |
XML-Specific Options
| Property | Default | Description |
|---|---|---|
CharacterEncoding |
"utf-8" |
Document encoding (ascii, utf-8, utf-16, utf-32, iso-8859-1). |
ByteOrderMark |
false |
Include a BOM in the stream output. |
BigEndian |
false |
Use big-endian byte order for multi-byte encodings. |
AddDocumentDeclaration |
true |
Include the XML declaration. |
OmitDuplicateNamespaces |
true |
Suppress redundant namespace declarations. |
AttributesOnNewLine |
false |
Place each attribute on its own line. |
JSON-Specific Options
| Property | Default | Description |
|---|---|---|
AllowTrailingCommas |
false |
Allow trailing commas in input JSON. |
Options can be passed to the transform constructor:
var options = new XmlOptions
{
Indent = true,
IndentSize = 4,
Identifiers = IdentifierConventions.Camel,
ValidateInputDocuments = ValidateExpressionDocuments.Never,
};
var transform = new ExpressionXmlTransform(options);
Schema Validation
Both the XML and JSON serialization packages ship with embedded schemas that formally describe the structure of their
serialized documents. The XML package includes three XSD schemas
(Linq.Expressions.Serialization.xsd, DataContract.xsd, Microsoft.Serialization.xsd); the JSON package includes a JSON
Schema (Linq.Expressions.Serialization.json).
In many scenarios validation is unnecessary. When both the producer and consumer of a serialized expression use the same
version of these packages, the output is guaranteed to conform to the schema. Turning validation off
(ValidateInputDocuments = Never) eliminates the parsing overhead entirely and is the recommended setting for production
workloads where both ends are under your control. Validation becomes valuable when the serialized document acts as a
contract boundary — for example, when documents are stored long-term and may outlive the producing code, when they cross
trust boundaries between independently deployed services, or when third-party tools generate or modify the documents.
Performance impact can be substantial, especially for JSON deserialization with strict schema validation.
In benchmark runs, JSON deserialize + ValidateInputDocuments = Always is often orders of magnitude slower than
Never, while XML validation overhead is typically much smaller. If performance is important, prefer:
ValidateInputDocuments = Neverfor trusted/internal documents.- XML format when strict validation is required on hot paths.
Validation is demand-driven: schema evaluation only runs when MustValidate is true (that is, when validation is enabled
and a schema is available). The schema libraries are still linked into the build, but per-document validation work is not
performed unless requested.
The JSON schema validation uses JsonSchema.Net. It handles the vast
majority of expression patterns correctly but has known edge cases with deeply nested recursive $ref combined with oneOf
constructs — currently 2 out of 718 tested expression patterns produce false validation failures on documents that are in
fact valid. If your project requires complete JSON validation fidelity, you can clone the source, define the
NEWTONSOFT_SCHEMA preprocessor symbol in the build configuration, and build against
Newtonsoft.Json.Schema (NSJ) instead. NSJ is commercially licensed: the free tier
allows 1,000 validations per hour; a paid license is required for higher throughput. The XML schema validation uses the
built-in System.Xml.Schema infrastructure and has no known issues.
Security Considerations
Deserializing a LINQ expression tree reconstructs executable code: the resulting Expression can be compiled into a delegate
and invoked. This makes expression documents a potential vector for remote code execution if an attacker can supply or
tamper with the input.
Risks
- Arbitrary type instantiation — the document contains assembly-qualified type names that are resolved via reflection. A crafted document could reference types the consumer did not intend to load.
- Method invocation —
MethodCallandInvocationnodes can describe calls to any accessible method, including process-level operations (Process.Start, file I/O, network access). - Constant injection —
Constantnodes can carry arbitrary literal values, including connection strings, credentials, or other sensitive data designed to be captured by a downstream lambda closure.
Mitigations
Treat serialized expressions as untrusted code. Apply the same scrutiny you would give to a dynamically loaded assembly. Never deserialize expression documents from unauthenticated or unverified sources.
Wrap the document in a signed envelope. Before transmitting or persisting a serialized expression, embed it inside a signed wrapper — for example, a JWS (JSON Web Signature) or XML Digital Signature envelope. The consumer verifies the signature before deserializing. This ensures the document has not been tampered with in transit or at rest.
Attach security metadata. Insert additional properties or elements into the document (or its envelope) that describe the permitted execution context — e.g., an allow-list of assemblies and types, a maximum expression depth, or a principal/role that is authorized to evaluate the expression. The consumer checks these properties before compiling or invoking the result.
Restrict the type universe. After deserialization, walk the resulting
Expressiontree and verify that everyTypereference and everyMethodInfobelongs to an approved allow-list. Reject the expression if any node references an unexpected type or method.Run in a sandboxed context. If you must evaluate expressions from less-trusted sources, compile and invoke them inside a restricted environment — a separate process with limited permissions, a container, or an AppDomain-equivalent isolation boundary — to contain the blast radius of a malicious expression.
Schema validation does not provide security. A document can be schema-valid yet still contain harmful type references or method calls. Security requires authentication, integrity verification, and runtime constraints as described above.
Get the Code
Clone the GitHub repository:
git clone https://github.com/vmelamed/vm2.Linq.Expressions.git
Build from the Source Code
dotnet build
Or build a specific project:
dotnet build src/Serialization.Xml/Serialization.Xml.csproj
Tests
The test projects are in the test/ directory. They use MTP v2 with xUnit v3. Tests are buildable and runnable from the
command line and from Visual Studio Code.
dotnet test
Or run a specific test project:
dotnet test test/Serialization.Xml.Tests/Serialization.Xml.Tests.csproj
Related Packages
- vm2.Glob.Api — Cross-platform glob pattern matching
- vm2.Ulid — ULID generation and parsing
- vm2.SemVer — Semantic versioning
License
MIT — See LICENSE
Version History
See CHANGELOG.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. 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. |
-
net10.0
- JsonSchema.Net (>= 9.2.0)
- Microsoft.Extensions.Configuration.Binder (>= 10.0.6)
- Microsoft.Extensions.Configuration.CommandLine (>= 10.0.6)
- Microsoft.Extensions.Configuration.EnvironmentVariables (>= 10.0.6)
- Microsoft.Extensions.Configuration.Json (>= 10.0.6)
- Microsoft.Extensions.DependencyInjection (>= 10.0.6)
- Microsoft.Extensions.Hosting (>= 10.0.6)
- Microsoft.Extensions.Logging (>= 10.0.6)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.6)
- Microsoft.Extensions.Logging.Configuration (>= 10.0.6)
- Microsoft.Extensions.Logging.Console (>= 10.0.6)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.6)
- Newtonsoft.Json (>= 13.0.4)
- System.Configuration.ConfigurationManager (>= 10.0.6)
- vm2.Linq.Expressions.Serialization.Abstractions (>= 1.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.1.0 | 0 | 5/21/2026 |
| 1.1.0-preview.1 | 0 | 5/21/2026 |
| 1.0.2 | 93 | 4/24/2026 |
| 1.0.2-preview.11 | 51 | 4/24/2026 |
| 1.0.2-preview.6 | 64 | 4/23/2026 |
| 1.0.2-preview.5 | 56 | 4/22/2026 |
| 1.0.2-preview.4 | 53 | 4/22/2026 |
| 1.0.2-preview.3 | 48 | 4/22/2026 |
| 1.0.2-preview.2 | 48 | 4/22/2026 |
| 1.0.2-preview.1 | 50 | 4/21/2026 |
| 1.0.1 | 98 | 4/20/2026 |
| 1.0.0 | 111 | 4/20/2026 |
| 1.0.0-preview.2 | 53 | 4/20/2026 |
| 1.0.0-preview.1 | 49 | 4/20/2026 |
v1