A simple, lightweight way to work with JSON as dynamic objects or lists, while still giving you type safety when you need it.
This library converts JSON into DynamicJsonObject and DynamicJsonList, enabling natural property access while retaining optional mapping to strongly typed POCOs.
-
json.ToDynamic()entry point
Converts JSON into a dynamic object or list that behaves predictably in .NET. -
Straightforward property access
Case-insensitive lookups with safe null returns for missing fields. -
Lists integrate naturally with .NET
Dynamic lists support indexing and can be used directly with LINQ. -
Automatic handling of JSON primitives
Strings, numbers, booleans, and null values map directly to .NET types. -
Object mapping with
AsType<T>()
Converts dynamic objects into POCOs using simple reflection-based mapping. -
Scalar list conversion (
ToScalarList<T>())
Extracts arrays of primitives (e.g., strings, ints) into strongly typed lists. -
Object list conversion (
ToList<T>())
Converts arrays of JSON objects intoList<T>without extra serializer configuration. -
Clear, predictable error behavior
Missing properties return null; invalid casts are skipped; index errors throw normally. -
Round-trip JSON support (
ToJson())
Modified dynamic objects can be serialized back to JSON cleanly. -
Minimal, focused API surface
Provides practical capabilities without a large configuration model. -
Diff / Patch / Merge utilities
Built-in helpers for comparing and combining JSON structures.
using WilliamSmithE.DynamicJson;
string json = @"
{
""id"": 67,
""name"": ""John Doe"",
""isActive"": true,
""createdDate"": ""2025-01-15T10:45:00Z"",
""profile"": {
""email"": ""[email protected]"",
""department"": ""Engineering"",
""roles"": [
{ ""roleName"": ""Admin"", ""level"": 5 },
{ ""roleName"": ""Developer"", ""level"": 3 }
]
},
""preferences"": {
""theme"": ""dark"",
""dashboardWidgets"": [ ""inbox"", ""projects"", ""metrics"" ]
}
}
";
var dynObj = json.ToDynamic();Console.WriteLine(dynObj.id); // 67
Console.WriteLine(dynObj.name); // John Doe
Console.WriteLine(dynObj.profile.email); // [email protected]
var firstRole = dynObj.profile.roles.First();
Console.WriteLine(firstRole.roleName); // AdminDynamicJson automatically normalizes all JSON property names using a simple rule:
By default: Only letters and digits are kept. All other characters are removed. (A–Z, a–z, 0–9)
Examples:
| JSON Key | Sanitized Form |
|---|---|
First Name |
FirstName |
PROJECT NAME |
PROJECTNAME |
order-id |
orderid |
2024_total$ |
2024total |
This means you can safely access JSON like:
{
"First Name": "Harry"
"order-id": 12345
}Using:
dynObj.FirstName // "Harry"
dynObj.OrderId // 12345You can supply a Func<char, bool> delegate that determines which characters are retained:
// Example: allow letters, digits, underscores, and hyphens
Func<char, bool> filter = c =>
char.IsLetterOrDigit(c) || c == '_' || c == '-';
var obj = new DynamicJsonObject(values, filter);
var sanitized = originalKey.Sanitize(filter);After keys are sanitized, duplicates are automatically renamed by adding a numeric suffix:
-> The first occurrence keeps its name, and any additional collisions become key2, key3, and so on. This ensures every property remains unique without losing any values.
-> The order of properties is preserved as they appear in the original JSON.
Scalar properties:
using WilliamSmithE.DynamicJson;
var jsonString = """
{
"name": "John Doe",
"age": 30,
"job-title": "Analyst",
"jobTitle": "Senior Analyst",
"skills": ["C#", "JavaScript", "SQL"],
"address": {
"street": "123 Main St",
"city": "Anytown",
"zip": "12345"
}
}
""";
var dynObj = jsonString.ToDynamic();
Console.WriteLine(dynObj.JobTitle); // Analyst
Console.WriteLine(dynObj.JobTitle2); // Senior AnalystObject / Array properties:
using WilliamSmithE.DynamicJson;
var jsonString = """
{
"name": "John Doe",
"skills": ["C#", "JavaScript", "SQL"],
"Skills": ["Excel", "PowerBI", "Tableau"],
"Skills": ["SqlServer", "Kubernetes", "AWS"],
"Credentials": {
"username": "johndoe",
"password": "securepassword123"
},
"Credentials": {
"apiKey": "ABCD"
}
}
""";
var dyn = jsonString.ToDynamic();
Console.WriteLine(string.Join(", ", dyn.Skills)); // C#, JavaScript, SQL
Console.WriteLine(string.Join(", ", dyn.Skills2)); // Excel, PowerBI, Tableau
Console.WriteLine(string.Join(", ", dyn.Skills3)); // SqlServer, Kubernetes, AWS
Console.WriteLine(dyn.Credentials.Username + " | " + dyn.Credentials.Password); // johndoe | securepassword123
Console.WriteLine(dyn.Credentials2.ApiKey); // ABCDDynamicJson automatically maps JSON primitives and CLR value types into appropriate .NET types.
| JSON / CLR Value | Resulting DynamicJson Type | Notes |
|---|---|---|
123 |
long or double |
Integers stay long; large/float-like values become double. |
19.99 |
double or decimal |
Cast inside LINQ projections. |
\"2025-12-13T00:00Z\" |
DateTime |
ISO-like strings auto-parse to DateTime. |
true / false |
bool |
Direct mapping. |
null |
null |
Preserved. |
Use the .AsEnumerable() extension method to enable LINQ queries on DynamicJsonList objects.
⚠️ When using.AsEnumerable(...)with a dynamic list, cast the source toDynamicJsonListso the lambda can be bound correctly by the C# compiler.
Example:
string usersJson = """
{
"users": [
{
"name": "Alice",
"roles": [
{ "roleName": "Admin", "permissions": [ "read", "write", "delete" ] },
{ "roleName": "User", "permissions": [ "read" ] }
]
},
{
"name": "Bob",
"roles": [
{ "roleName": "Developer", "permissions": [ "read", "commit" ] },
{ "roleName": "User", "permissions": [ "read" ] }
]
}
]
}
""";
var dynObj = usersJson.ToDynamic();
var names =
((DynamicJsonList)dynObj.users)
.AsEnumerable()
.Where(u =>
((DynamicJsonList)u.roles)
.AsEnumerable()
.Any(r => r.roleName == "Admin")
)
.Select(u => (string)u.name)
.Distinct()
.OrderBy(x => x)
.ToList();
foreach (var name in names)
{
Console.WriteLine(name);
}Because AsEnumerable() produces IEnumerable<dynamic>, LINQ cannot infer the numeric type automatically.
This means:
- You must cast inside projection lambdas (e.g., for
Sum,Average,Max, etc.). - Without casting, LINQ will default to the
intoverload, which can cause runtime binder errors.
double price = (double)dynItem.Price;
long qty = (long)dynItem.Qty;
bool active = (bool)dynUser.IsActive;
DateTime ts = (DateTime)dynRecord.Timestamp;DynamicJson maps JSON to CLR objects using sanitized, case-insensitive property matching.
This means JSON like:
{
"Created Date": "1/1/2025"
}
OR
{
"Created-Date": "1/1/2025"
}Will correctly populate a POCO property named:
public DateTime CreatedDate { get; set; }public class MyClass
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public bool IsActive { get; set; }
public DateTime CreatedDate { get; set; }
}
MyClass instance = dynObj.AsType<MyClass>();
Console.WriteLine(instance.Id); // 67public class Profile
{
public string Email { get; set; } = string.Empty;
public string Department { get; set; } = string.Empty;
}
var profile = dynObj.profile.AsType<Profile>();
Console.WriteLine(profile.Department); // Engineeringvar profileJson = dynObj.profile.ToJson();
Console.WriteLine(profileJson);Or via helper:
var jsonOut = DynamicJson.ToJson(dynObj.preferences.dashboardWidgets);
Console.WriteLine(jsonOut);foreach (var role in dynObj.profile.roles)
{
Console.WriteLine(role.roleName);
}Indexing into a DynamicJsonList behaves like a normal .NET list:
Console.WriteLine(dynObj.profile.roles[0].roleName); // valid
Console.WriteLine(dynObj.profile.roles[5]);
// throws IndexOutOfRangeException with a clear messageMapping to POCOs:
public class Role
{
public string RoleName { get; set; } = string.Empty;
public int Level { get; set; }
}
var roles = dynObj.profile.roles.ToList<Role>();.ToScalarList():
using WilliamSmithE.DynamicJson;
var dyn = """
{
"Users": [
{ "Name": "Alice", "Age": 30, "Locations": ["Boston", "Chicago"] },
{ "Name": "Bob", "Age": 25, "Locations": ["New York", "Los Angeles"] }
]
}
""".ToDynamic();
Console.WriteLine((
(List<string>)dyn // Cast to List<string>
.Users // Access Users array
.First() // Get the first user
.Locations // Access Locations array
.ToScalarList<string>()) // Convert to List<string>
.Skip(1) // Get the second location
.First()); // Output: Chicagousing WilliamSmithE.DynamicJson;
// JSON comes from outside your system (HTTP, file, DB, etc.)
var customerJson = """
{
"CustomerId": 42,
"Name": "Jane Doe",
"Email": "[email protected]"
}
""";
var cartItemsJson = """
[
{ "Sku": "ABC123", "Qty": 1, "Price": 19.99 },
{ "Sku": "XYZ789", "Qty": 2, "Price": 5.00 }
]
""";
// 1) Convert JSON → dynamic JSON objects
dynamic customer = customerJson.ToDynamic();
var cartItems = (DynamicJsonList)cartItemsJson.ToDynamic();
customer.Name = "John Doe";
customer.Email = "[email protected]";
// Work with value types dynamically
var dynamicTotal = cartItems
.AsEnumerable()
.Sum(x => (long)x.Qty * (double)x.Price);
Console.WriteLine($"Dynamic cart total: {dynamicTotal}");
// 2) Build outbound payload as a CLR anonymous object
var payload = new
{
customer = Raw.ToRawObject(customer),
items = Raw.ToRawObject(cartItems),
total = dynamicTotal,
timestamp = DateTime.UtcNow
};
payload.customer.Name = "James Doe";
// 3) Convert entire payload → dynamic JSON
dynamic dyn = payload.ToDynamic();
// 4) Use the result dynamically
Console.WriteLine((string)dyn.customer.Name); // "John Doe"
Console.WriteLine((double)dyn.total); // 29.99 → double
Console.WriteLine((string)dyn.items[0].Sku); // "ABC123"
// 5) Modify before sending
dyn.customer.Email = "billing@" + dyn.customer.Email;
// 6) Serialize back for HTTP call
var finalJson = DynamicJson.ToJson(dyn);
Console.WriteLine("Final outbound JSON:");
Console.WriteLine(finalJson);
// Dynamic cart total: 29.99
// John Doe
// 29.99
// ABC123
// Final outbound JSON:
// {
// "customer": {
// "CustomerId": 42,
// "Name": "John Doe",
// "Email": "billing@[email protected]"
// },
// "items": [
// {
// "Sku": "ABC123",
// "Qty": 1,
// "Price": 19.99
// },
// {
// "Sku": "XYZ789",
// "Qty": 2,
// "Price": 5
// }
// ],
// "total": 29.99,
// "timestamp": "2025-12-13T09:47:40.4611875Z"
// }Diff compares two JSON values and produces a minimal change object that describes only what is different between them. It does not return the entire JSON structure. This represents the smallest set of updates needed to turn the first object into the second.
Example:
using WilliamSmithE.DynamicJson;
dynamic before = """
{
"Name": "Alice",
"Age": 30,
"City": "Boston"
}
""".ToDynamic();
dynamic after = """
{
"Name": "Alicia",
"Age": 31,
"City": "Boston"
}
""".ToDynamic();
// Compute the minimal diff between the two JSON values
dynamic patch = DynamicJson.DiffDynamic(before, after);
Console.WriteLine(DynamicJson.ToJson(patch));
// Output:
// {
// "Name": "Alicia",
// "Age": 31
// }Patch takes an original JSON value and a diff, and applies those changes to produce an updated JSON value.
Example:
using WilliamSmithE.DynamicJson;
dynamic before = """
{
"Name": "Alice",
"Age": 30,
"City": "Boston"
}
""".ToDynamic();
dynamic after = """
{
"Name": "Alicia",
"Age": 31,
"City": "Boston"
}
""".ToDynamic();
// First compute the diff
dynamic patch = DynamicJson.DiffDynamic(before, after);
// Apply the diff to the original
dynamic patched = DynamicJson.ApplyPatchDynamic(before, patch);
Console.WriteLine(DynamicJson.ToJson(patched));
// Output:
// {
// "Name": "Alicia",
// "Age": 31,
// "City": "Boston"
// }Merge combines two JSON values into a single result by overlaying the fields from the second value onto the first. Unlike ApplyPatch, which applies only changes, merge performs a full union of both JSON structures.
Example:
using WilliamSmithE.DynamicJson;
dynamic left = """
{
"Name": "Alice",
"Address": { "City": "Boston" },
"Tags": ["user"]
}
""".ToDynamic();
dynamic right = """
{
"Age": 30,
"Address": { "Zip": "02110" },
"Tags": ["admin"]
}
""".ToDynamic();
dynamic merged = DynamicJson.MergeDynamic(left, right);
Console.WriteLine(DynamicJson.ToJson(merged));
// Output:
// {
// "Name": "Alice",
// "Address": { "City": "Boston", "Zip": "02110" },
// "Tags": ["admin"],
// "Age": 30
// }
dynamic mergedConcat = DynamicJson.MergeDynamic(left, right, concatArrays: true);
Console.WriteLine(DynamicJson.ToJson(mergedConcat));
// Output with concatArrays = true:
// {
// "Name": "Alice",
// "Address": { "City": "Boston", "Zip": "02110" },
// "Tags": ["user", "admin"],
// "Age": 30
// }The Clone method creates a deep copy of the DynamicJson object, including all nested structures. This allows you to work with a copy of the data without affecting the original object.
Example:
using WilliamSmithE.DynamicJson;
dynamic original = """
{
"Name": "Alice",
"Age": 30,
"City": "Boston"
}
""".ToDynamic();
dynamic copy = original.Clone();
copy.Name = "Alicia";
Console.WriteLine(original.Name); // Output: Alice
Console.WriteLine(copy.Name); // Output: AliciaJsonPath is a value type that represents a specific location inside a JSON structure. It is designed to be composable, comparable, hashable, and enumerable.
Unlike string paths, a JsonPath is:
-
Built structurally
-
Compared structurally
-
Safe to use as a dictionary key
-
Independent of any particular JSON instance
Example:
using WilliamSmithE.DynamicJson;
var p1 = JsonPath.Root.Property("user").Property("orders").Index(0).Property("id");
var p2 = JsonPath.Root.Property("user").Property("orders").Index(1).Property("id");
var p3 = JsonPath.Root.Property("user").Property("orders").Index(0).Property("id");
Console.WriteLine(p1); // /user/orders[0]/id
Console.WriteLine(p2); // /user/orders[1]/id
Console.WriteLine(p1 == p3); // True
var dict = new Dictionary<JsonPath, string>
{
[p1] = "Order0",
[p2] = "Order1"
};
Console.WriteLine(dict[p3]); // Order0
foreach (var seg in p1)
{
Console.WriteLine(seg.Kind == JsonPath.SegmentKind.Property
? seg.PropertyName
: $"[{seg.ArrayIndex}]");
}
// Expected Output:
// /user/orders[0]/id
// /user/orders[1]/id
// True
// Order0
// user
// orders
// [0]
// idPath-aware diffs allow you to compare two JSON-like values and receive a precise list of changes, each annotated with the exact location where it occurred.
Instead of a single “changed” result, the diff reports added, removed, and modified values along with their JsonPath. This makes JSON mutations explicit, inspectable, and easy to log or reason about, while preserving the library’s existing diff semantics.
Example:
using WilliamSmithE.DynamicJson;
var original = new
{
user = new
{
orders = new[]
{
new { id = 10, price = 19.99m },
new { id = 11, price = 5.00m }
},
address = new { zip = "94105" }
}
}
.ToDynamic();
var updated = new
{
user = new
{
orders = new[]
{
new { id = 10, price = 24.99m }, // price changed (but array is atomic)
new { id = 11, price = 5.00m }
}
// address removed
},
metadata = new { lastUpdated = "2025-12-21" } // added
}
.ToDynamic();
var changes = DynamicJson.DiffWithPaths(original, updated);
foreach (var c in changes)
{
Console.WriteLine($"{c.Kind,-9} {c.Path} | {DynamicJson.ToJson(c.OldValue)} -> {DynamicJson.ToJson(c.NewValue)}");
}
// Expected output:
// Modified / user / orders | [{ "id":10,"price":19.99},{ "id":11,"price":5}] -> [{"id":10,"price":24.99},{ "id":11,"price":5}]
// Removed / user / address | { "zip":"94105"} -> null
// Added / metadata | null-> { "lastUpdated":"2025-12-21T00:00:00"}JsonPathNavigation bridges JsonPath and the DynamicJson model. It lets you take a path and resolve it against a dynamic JSON value to retrieve whatever exists at that location.
- The result may be a primitive, an object, or an array, and it is returned in the same raw form used throughout DynamicJson.
- This makes paths produced by diffs or diagnostics immediately usable, allowing you to locate and inspect the exact data they refer to without re-parsing or manual navigation.
Example:
using WilliamSmithE.DynamicJson;
var json = new
{
user = new
{
orders = new[]
{
new { id = 10, price = 19.99m },
new { id = 11, price = 5.00m }
}
}
}
.ToDynamic();
var pathThatExists = JsonPath.Root
.Property("user")
.Property("orders")
.Index(0)
.Property("price");
if (JsonPathNavigation.TryGetAtPath(json, pathThatExists, out object? value))
Console.WriteLine(value); // 19.99
Console.WriteLine(JsonPathNavigation.GetAtPath(json, pathThatExists)); // 19.99
var pathThatDoesNotExist = JsonPath.Root
.Property("user")
.Property("orders")
.Index(2)
.Property("price");
if (!JsonPathNavigation.TryGetAtPath(json, pathThatDoesNotExist, out object? _))
Console.WriteLine("Path not found"); // Path not found
try
{
JsonPathNavigation.GetAtPath(json, pathThatDoesNotExist);
}
catch (KeyNotFoundException)
{
Console.WriteLine("Path not found"); // Path not found
}
var pathToOrders = JsonPath.Root
.Property("user")
.Property("orders");
var orders = JsonPathNavigation.GetAtPath(json, pathToOrders);
Console.WriteLine(DynamicJson.ToJson(orders)); // [{"id":10,"price":19.99},{"id":11,"price":5}]
var pathToUser = JsonPath.Root.Property("user");
var user = JsonPathNavigation.GetAtPath(json, pathToUser); // {"orders":[{"id":10,"price":19.99},{"id":11,"price":5}]}
Console.WriteLine(DynamicJson.ToJson(user));JsonPath.Parse converts a canonical path string into a JsonPath instance that behaves exactly like one built fluently in code.
- Parsed paths can be compared, enumerated, and resolved against DynamicJson values, making them useful for replaying or inspecting paths captured in logs, diagnostics, or configuration.
- The parser is intentionally strict and fails fast on invalid or ambiguous input to keep path handling predictable.
using WilliamSmithE.DynamicJson;
var json = new
{
user = new
{
orders = new[]
{
new { id = 10, price = 19.99m }
}
}
}
.ToDynamic();
var path = JsonPath.Parse("/user/orders[0]/price");
Console.WriteLine(path); // /user/orders[0]/price
var value = JsonPathNavigation.GetAtPath(json, path);
Console.WriteLine(value); // 19.99
Console.WriteLine(JsonPath.Parse("/").IsRoot); // True
try
{
JsonPath.Parse("user/orders");
}
catch (FormatException)
{
Console.WriteLine("Invalid"); // Invalid
}
try
{
JsonPath.Parse("/orders[-1]");
}
catch (FormatException)
{
Console.WriteLine("Invalid"); // Invalid
}
if (JsonPath.TryParse("/user/orders[0]/price", out var path2))
{
var value2 = JsonPathNavigation.GetAtPath(json, path2);
Console.WriteLine(value2); // 19.99
}
if (!JsonPath.TryParse("/user/order[]", out _)) // Invalid
{
Console.WriteLine("Invalid");
}IsValidFor provides a simple way to check whether a JSON path can be safely used against a specific DynamicJson value.
- It verifies not only that a path is syntactically valid, but also that it actually resolves within the given JSON structure.
- Useful when paths come from user input, configuration, or diagnostics and you need to ensure they refer to real data before attempting to read or act on them.
- By combining parsing and resolution into a single non-throwing check, IsValidFor keeps path validation explicit and predictable without altering the underlying JSON or path semantics.
Example:
using WilliamSmithE.DynamicJson;
var json = new
{
user = new
{
orders = new[]
{
new { id = 10, price = 19.99m }
}
}
}
.ToDynamic();
if (JsonPathValidation.IsValidFor(json, "/user/orders[0]/price"))
{
Console.WriteLine("Path exists in this JSON");
Console.WriteLine(JsonPathNavigation.GetAtPath(json, "/user/orders[0]/price"));
Console.WriteLine();
}
if (!JsonPathValidation.IsValidFor(json, "/user/order"))
{
Console.WriteLine("Path is valid syntax, but not valid for this JSON");
}
if (!JsonPathValidation.IsValidFor(json, "/user/orders[2]/price"))
{
Console.WriteLine("Path is valid syntax, but does not exist in this Json");
}MIT License. See LICENSE file for details.