A simple, lightweight way to work with JSON as dynamic objects or lists, while still giving you type safety when you need it.
$ dotnet add package WilliamSmithE.DynamicJsonA 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 into List<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"": ""john@doe.com"",
""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); // john@doe.com
var firstRole = dynObj.profile.roles.First();
Console.WriteLine(firstRole.roleName); // Admin
DynamicJson 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 // 12345
You 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 Analyst
Object / 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); // ABCD
DynamicJson 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);
}
⚠️ Casting Disclaimer:
Because AsEnumerable() produces IEnumerable<dynamic>, LINQ cannot infer the numeric type automatically.
This means:
Sum, Average, Max, etc.).int overload, 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); // 67
public 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); // Engineering
var 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 message
Mapping 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: Chicago
using WilliamSmithE.DynamicJson;
// JSON comes from outside your system (HTTP, file, DB, etc.)
var customerJson = """
{
"CustomerId": 42,
"Name": "Jane Doe",
"Email": "jane@example.com"
}
""";
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 = "john@example.com";
// 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@john@example.com"
// },
// "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: Alicia
JsonPath 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]
// id
Path-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.
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(DynamicJson.ToJson(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.
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(DynamicJson.ToJson(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(DynamicJson.ToJson(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.
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.