April 23rd, 2021.
This document covers the API and design for a writable DOM along with support for the C# dynamic
keyword.
UPDATE: this feature is now in main for Preview 4 along with a follow-up PR that will be in Preview 5.
It is expected that a significant percent of existing System.Text.Json consumers will use these new APIs, and also attract new consumers including:
- A need for a lightweight, simple API especially for one-off cases.
- A need for
dynamic
capabilities for varying reasons including sharing of loosely-typed, script-based code. - To efficiently read or modify a subset of a large tree. For example, it is possible to efficiently navigate to a subsection of a large tree and read an array or deserialize a POCO from that subsection. LINQ can also be used with that.
- Those unable or unwilling to use the serializer for varying reasons:
- Too heavyweight; requires compilation of POCO types.
- Limitations in the serializer such as polymorphism or to address denormalization scenarios.
- JSON schema is not fixed and must be inspected.
- Those wanting to extend serializer capabilities within a custom converter. For example, to work around serializer limitations such as property ordering or flexible POCO constructors. In this case, the
Read()
and\orWrite()
methods can use nodes instead of or in addition to POCOs.
A prototype for 6.0 is available at https://github.com/steveharter/runtime/tree/WriteableDomAndDynamic.
Represented by an abstract base class JsonNode
along with derived classes for objects, arrays and values:
namespace System.Text.Json.Node
{
public abstract class JsonNode {...};
public sealed class JsonObject : JsonNode, IDictionary<string, JsonNode?> {...}
public sealed class JsonArray : JsonNode, IList<JsonNode?> {...};
public abstract class JsonValue : JsonNode {...};
}
The existing JsonDocument
and JsonElement
types represent the DOM support today which is read-only. JsonDocument
maintains a single immutable UTF-8 buffer and returns JsonElement
value types from that buffer on demand. That design minimizes the initial JsonDocument.Parse()
time and associated heap allocs but also makes it slow to re-obtain the same value (which is common with LINQ) and does not lend itself to being directly extended to support writability.
Although the internal UTF-8 buffer will continue to be immutable with the design proposed here, a JsonElement
will indirectly support writability by forwarding serialization to a linked JsonNode
. When a linked JsonElement
is serialized, it will forward to the JsonNode
. This JsonNode
interop is not intended as the primary "writeable DOM" API, but is useful for scenarios that already use JsonElement
.
Currently there is no direct support for dynamic
in System.Text.Json. Adding support for that implies adding a writeable DOM. The design proposed here considers both dynamic
and non-dynamic
scenarios for a writeable DOM which allows for a common API, shared code and intuitive interop between the two.
This design is based on learning and scenarios from these reference implementations:
- The writable DOM prototype.
- During 5.0, there was a writable DOM effort. The design centered around writable aspects, usability and LINQ support. It was shelved due to time constraints and outstanding work.
- What's the same as the design proposed here:
- The high-level class names
JsonNode
,JsonArray
andJsonObject
. - Interaction with
JsonElement
.
- The high-level class names
- What's different:
JsonValue
instead ofJsonString
,JsonNumber
andJsonBoolean
.- The writable
JsonValue
internal value is based on CLR types, not a string representing JSON. For example, aJsonValue
initialized with anint
keeps thatint
value without boxing or converting to a string-based field.
- What's the same as the design proposed here:
- During 5.0, there was a writable DOM effort. The design centered around writable aspects, usability and LINQ support. It was shelved due to time constraints and outstanding work.
- The dynamic support code example. During 5.0, dynamic support was not able to be implemented due to schedule constraints. Instead, a code example was provided that enables
dynamic
and a writeable DOM that can be used by any 3.0+ runtime in order to unblock the community and also has been useful in gathering feedback for the design proposed here. - Azure prototyping. The Azure SDK team needs a writable DOM, and supporting
dynamic
is important for some scenarios but not all. A prototype has been created. Work is also being coordinated with the C# team to support Intellisense withdynamic
, although that is not expected to be implemented in time to be used in 6.0. - Newtonsoft's Json.NET. This has a similar
JToken
class hierarchy and support fordynamic
. TheJToken
hierarchy includes a singleJValue
type to represent known value types. Json.NET also has implicit and explicit operators similar as to what is being proposed here.- Converting code using Json.NET to System.Text.Json should be fairly straightforward although not all Json.NET features are implemented. See the "Features not proposed in first round" section for more information.
- The
JsonValue
proposed here supports assigning any value (custom data types, POCOs, collection, anonymous types) which will be serialized appropriately; this is useful for building JSON to match non-trivial contracts. This is not possible with Json.NET as it only allows "known" primitive types forJValue
along withJObject
andJArray
for objects and collections.
During deserialization, JsonNode
uses JsonElement
.
During serialization, JsonNode
uses JsonElement
if the values are backed by JsonElement
otherwise JsonNode
uses JsonSerializer
.
Layering on JsonSerializer
is necessary to support dynamic
since arbitrary CLR types, POCOs, collections, anonymous types, etc can be assigned to dynamic object properties and array elements and these are expected to serialize. This layering also supports the use serialization features including custom converters that support (de)serialization of custom data types.
- The
JsonValue
class supports specifying any CLR type that the standard serializer supports. This makes generating JSON easy for calling services, etc. This includes serialization support for:- Custom data types registered with
JsonSerializerOptions
. - C# Anonymous types.
- All POCOs and collection types.
- Other serializer features including quoted numbers.
- Custom data types registered with
- Support for C#
dynamic
and ability to invokeJsonNode
methods includingGetValue()
etc. - Performance
- A
JsonNode.Parse()
method is based on a singleJsonElement
which is very efficient: a single alloc is used to maintain entire JSON buffer and no materialized child elements until necessary. - Obtaining primitive values from a backing
JsonElement
is very efficient: delayed creation of strings etc untilGetValue()
is called. - After
Parse()
, it is possible to navigate to a deep child node with minimal allocations: only the branches navigated are created, and those are backed byJsonElement
. - Can work with
JsonDocument
dispose pattern and pooled buffers to prevent the potentially large JSON alloc. - Once a
JsonObject
is backed by aList<>
(e.g. in edit mode or when the singleJsonElement
from Parse() is converted to aList<>
), it will create and maintain a dictionary once a property count threshold is hit. UsingList<>
until that threshold prevents an expensive dictionary creation (in time and allocs) and thus improves CPU performance for smaller POCOs. UsingDictionary<>
for property lookup once that threshold is hit improves property access performance for larger POCOs.
- A
- Programming model
- Common programming model to obtain values whether backed by a
JsonElement
afterParse()
or backed by an actual CLR type in "edit" mode. e.g.jvalue.GetValue<int>()
works in either case. - A POCO property or collection element can be a
JsonNode
or a derived type. - The extension data property (to capture extra JSON properties that don't map to a CLR type) can now be
JsonObject
instead ofDictionary<string, JsonElement>
. - Ability to deserialize a member declared as
System.Object
asJsonNode
instead ofJsonElement
. - Ability to obtain a
JsonNode
from aJsonElement
via JsonNode constructors that take aJsonElement
. JsonObject
has deterministic enumeration and property ordering during serialization which is based on an internalList<>
; it is not based on non-deterministic dictionary ordering. New elements are added at the end.
- Common programming model to obtain values whether backed by a
- Debugging
ToString()
returns formatted JSON for easy inspection (similar toJsonElement.ToString()
). Note thatToJsonString()
should be used to obtain round-trippable, terse JSON.GetPath()
can be used to determine where a given node is at in a tree.- The attribute
[DebuggerTypeProxy]
is used forJsonNode
-derived classes to display the JSON, Path and property\item counts. The JSON will be astring
property to allow usage of the JSON visualizer window.
- LINQ
IEnumerable<JsonNode>
-basedJsonObject
andJsonNode
.Parent
andRoot
properties to support querying against relationships and to support a single globalJsonNodeOptions
.
A deserialized JsonNode
value internally holds a JsonElement
which knows about the JSON kind (object, array, number, string, true, false) and the raw UTF-8 value including any child nodes (for a JSON object or array).
When the consumer obtains a primitive value through jvalue.GetValue<double>()
, for example, the CLR double
value is deserialized from the raw UTF-8. Thus the mapping between UTF-8 JSON and the CLR object model is deferred until manually mapped by the consumer by specifying the expected type.
Deferring is consistent with existing JsonElement
semantics where, for example, a JSON number is not automatically mapped to any CLR type until the consumer calls a method such as JsonElement.GetDecimal()
or JsonElement.GetDouble()
. This design is preferred over eager "guessing" what the CLR type should be based upon the contents of the JSON such as whether the JSON contains a decimal point or the presence of an exponent. Guessing can lead to issues including overflow\underflow or precision loss.
In addition to deferring the creation of numbers in this manner, the other node types are also deferred including strings. In addition, JsonObject
and JsonArray
instances are not populated with child nodes until traversed. Using JsonElement
in this way along with deferred creation of values and child nodes greatly improves CPU performance and reduces allocations especially when a subset of a tree is traversed.
Simple deserialization example:
JsonNode jObject = JsonNode.Parse("{""MyProperty"":42}");
JsonValue jValue = jObject["MyProperty"];
// Verify the contents
Debug.Assert(jObject is JsonObject);
Debug.Assert(jValue is JsonValue); // On deserialize, the value is JsonElement
int i = (int)jValue; // Same programming model as the sample below; shortcut for "jValue.GetValue<int>()"
Debug.Assert(i == 42);
To mutate the value, a new node instance must be created:
jObject["MyProperty"] = 43;
// Verify the contents
JsonValue jValue = jObject["MyProperty"];
Debug.Assert(jValue is JsonValue);
int i = (int)jValue; // Same programming model as the sample above.
Debug.Assert(i == 43);
Note that in both cases an explicit cast to int
is used: this is important because it means there is a common programming model for cases when a given JsonValue
is backed by JsonElement
(for read mode \ deserialization) or backed by an actual value such as int
(for edit mode or serialization). In this example, the consumer always knows that the "MyProperty" number property can be returned as an int
when given a JsonValue
and doesn't have to be concerned about whether it is backed by an int
or JsonElement
.
An explicit operator was used in the above example:
int i = (int)jValue;
which expands to:
int i = jValue.GetValue<int>();
An implicit operator was used in the above example:
jObject["MyProperty"] = 43;
which expands to:
jObject["MyProperty"] = JsonValue.Create(43);
A JsonValue
supports custom converters that were added via JsonSerializerOptions.Converters.Add()
or specified in the JsonValue()
constructor. A common use case would be to add the JsonStringEnumConverter
which serializes Enum
values as a string instead of an integer, but also supports user-defined custom converters:
JsonNode jObject = JNode.Parse("{\"Amount\":1.23}");
JsonValue jValue = jObject["Amount"];
Money money = jValue.GetValue<Money>(options);
The call to GetValue<Money>()
above calls the custom converter for Money
.
During serialization, some features specified on JsonSerializerOptions
are supported including quoted numbers and null handling. See the "Interop" section.
Serialization can be performed in several ways:
- Calling
string json = jNode.ToJsonString()
. - Calling
jNode.WriteTo(utf8JsonWriter)
- Assigning a node to a POCO or adding to a collection and serializing that.
- Using the existing
JsonSerializer.Serialize<JsonNode>(jNode)
methods. - Obtaining a
JsonElement
from a node and serializing that throughjElement.WriteTo(utf8JsonWriter)
.
An assumption is that an explicit model is supported but not acceptable for tree traversal:
int i = ((JsonArray((JsonObject)((JsonObject)jNode)["Child"])["Array"])[1].GetValue<int>();
Compare that explicit model to using dynamic
:
int i = jNode.Child.Array[1];
The JsonNode
API proposed here aligns with the dynamic
syntax above plus and makes the most common scenarios look like:
int i = jNode["Child"]["Array"][1].GetValue<int>();
// or
int i = (int)jNode["Child"]["Array"][1];
This terse syntax works by adding the following helper methods to JsonNode:
public virtual JsonNode? this[int index] { get; set; } // for JsonArray
public virtual JsonNode? this[string propertyName] { get; set; } // for JsonObject
public virtual TValue? GetValue<TValue>(); // for JsonValue
However, no other helper methods such .Add()
is present (for JsonArray
).
Adding "As*()" methods would allow easier navigation to JsonArray.Add()
and others without casting. For example, here's a verbose way to obtain the value using the new "As*()" methods and without using the 3 helper methods above.
int i = jNode.AsObject()["Child"].AsObject()["Array"].AsArray()[1].GetValue<int>();
With the "As*()" methods, Add() can be called inline in one line of code and without casting:
JsonObject jObjectToAdd = ...
jNode["Child"]["Array"].AsArray().Add(jObjectToAdd);
The "As*()" methods are also used to remove Parse()
overloads on JsonArray
, JsonObject
and JsonValue
:
// This:
JsonObject jObject = JsonNode.Parse(...).AsJsonObject();
// is an in-line alternative to:
JsonObject jObject = (JsonObject)JsonNode.Parse(...);
All types live in System.Text.Json.dll
.
The proposed namespace is "System.Text.Json.Node" although "System.Text.Json" is also a suitable namespace since it contains the existing JsonDocument
, JsonElement
and JsonSerializer
classes.
namespace System.Text.Json.Node
{
public abstract class JsonNode : System.Dynamic.IDynamicMetaObjectProvider
{
internal JsonNode(); // prevent external derived classes.
// Options specified during Parse() or in a constructor.
// Normally only specified in root node.
// If null, Parent.Options are used (recursively). If Root.Options is null, default options used.
public JsonNodeOptions? Options { get; }
// Alternative to casting to allow for in-line dot syntax.
// Throws InvalidOperationException on mismatch.
public JsonArray AsArray();
public JsonObject AsObject();
public JsonValue AsValue();
// JsonArray terse syntax support.
// Throws InvalidOperationException on non-JsonArray instances.
// Use AsArray() to get access members other than this indexer.
public virtual JsonNode? this[int index] { get; set; }
// JsonObject terse syntax.
// Throws InvalidOperationException on non-JsonObject instances.
// Use AsObject() to get access members other than this indexer.
public virtual JsonNode? this[string propertyName] { get; set; }
// JsonValue terse syntax.
// Throws InvalidOperationException on non-JsonValue instances.
// Use AsValue() to get access members other than this method.
// Returns the internal value, a JsonElement conversion, or a custom conversion to the provided type.
// Allows for common programming model when JsonValue is based on JsonElement or a CLR value.
// "TValue" vs "T" to prevent collision with JsonValue.
public virtual TValue? GetValue<TValue>();
// Return the parent and root nodes; useful for LINQ.
public JsonNode? Parent { get; }
public JsonNode Root { get; }
// The JSON Path; same "JsonPath" syntax we use for JsonException information.
// Not a simple calculation since nodes can change order in JsonArray.
public string GetPath();
// Not to be used as deserializable JSON.
// - Pretty printed (JsonWriterOptions.Indented).
// - A string-based root JsonValue will not be quoted.
public override string ToString();
// Serialize as JSON that can later be used to deserialize.
public string ToJsonString(JsonSerializerOptions? options = null);
// Serialize as Utf8
public byte[] ToUtf8Bytes(JsonSerializerOptions? options = null);
// Wrappers over ReadFrom(JsonDocument.Parse().RootElement.Clone()) for common string- and byte- based deserialization:
public static JsonNode? Parse(string json,
JsonNodeOptions? nodeOptions = null,
JsonDocumentOptions documentOptions = default(JsonDocumentOptions));
public static JsonNode? Parse(ReadOnlySpan<byte> utf8Json,
JsonNodeOptions? nodeOptions = null,
JsonDocumentOptions documentOptions = default(JsonDocumentOptions));
public static JsonNode? Parse(ref Utf8JsonReader reader,
JsonNodeOptions? nodeOptions = null);
public static JsonNode? Parse(Stream utf8Json,
JsonNodeOptions? nodeOptions = null,
JsonDocumentOptions documentOptions = default(JsonDocumentOptions));
public abstract void WriteTo(
Utf8JsonWriter writer,
JsonSerializerOptions? options = null);
// Dynamic support; implemented explicitly to help hide.
System.Dynamic.DynamicMetaObject System.Dynamic.IDynamicMetaObjectProvider.GetMetaObject(System.Linq.Expressions.Expression parameter);
// Explicit operators (can throw) from known primitives.
public static explicit operator bool(JsonNode value);
public static explicit operator byte(JsonNode value);
public static explicit operator char(JsonNode value);
[CLSCompliantAttribute(false)]
public static explicit operator DateTime(JsonNode value);
public static explicit operator DateTimeOffset(JsonNode value);
public static explicit operator decimal(JsonNode value);
public static explicit operator double(JsonNode value);
public static explicit operator Guid(JsonNode value);
public static explicit operator short(JsonNode value);
public static explicit operator int(JsonNode value);
public static explicit operator long(JsonNode value);
[CLSCompliantAttribute(false)]
public static explicit operator sbyte(JsonNode value);
public static explicit operator float(JsonNode value);
public static explicit operator ushort(JsonNode value);
[CLSCompliantAttribute(false)]
public static explicit operator uint(JsonNode value);
[CLSCompliantAttribute(false)]
public static explicit operator ulong(JsonNode value);
public static explicit operator bool?(JsonNode value);
public static explicit operator byte?(JsonNode value);
public static explicit operator char?(JsonNode value);
[CLSCompliantAttribute(false)]
public static explicit operator DateTime?(JsonNode value);
public static explicit operator DateTimeOffset?(JsonNode value);
public static explicit operator decimal?(JsonNode value);
public static explicit operator double?(JsonNode value);
public static explicit operator Guid?(JsonNode value);
public static explicit operator short?(JsonNode value);
public static explicit operator int?(JsonNode value);
public static explicit operator long?(JsonNode value);
[CLSCompliantAttribute(false)]
public static explicit operator sbyte?(JsonNode value);
public static explicit operator float?(JsonNode value);
public static explicit operator string?(JsonNode value);
public static explicit operator ushort?(JsonNode value);
[CLSCompliantAttribute(false)]
public static explicit operator uint?(JsonNode value);
[CLSCompliantAttribute(false)]
public static explicit operator ulong?(JsonNode value);
// Implicit operators (won't throw) from known primitives.
public static implicit operator JsonNode(bool value);
public static implicit operator JsonNode(byte value);
public static implicit operator JsonNode(char value);
public static implicit operator JsonNode(DateTime value);
public static implicit operator JsonNode(DateTimeOffset value);
public static implicit operator JsonNode(decimal value);
public static implicit operator JsonNode(double value);
public static implicit operator JsonNode(Guid value);
public static implicit operator JsonNode(short value);
public static implicit operator JsonNode(int value);
public static implicit operator JsonNode(long value);
[CLSCompliantAttribute(false)]
public static implicit operator JsonNode(sbyte value);
public static implicit operator JsonNode(float value);
[CLSCompliantAttribute(false)]
public static implicit operator JsonNode(ushort value);
[CLSCompliantAttribute(false)]
public static implicit operator JsonNode(uint value);
[CLSCompliantAttribute(false)]
public static implicit operator JsonNode(ulong value);
public static implicit operator JsonNode?(bool? value);
public static implicit operator JsonNode?(byte? value);
public static implicit operator JsonNode?(char? value);
public static implicit operator JsonNode?(DateTime? value);
public static implicit operator JsonNode?(DateTimeOffset? value);
public static implicit operator JsonNode?(decimal? value);
public static implicit operator JsonNode?(double? value);
public static implicit operator JsonNode?(Guid? value);
public static implicit operator JsonNode?(short? value);
public static implicit operator JsonNode?(int? value);
public static implicit operator JsonNode?(long? value);
[System.CLSCompliantAttribute(false)]
public static implicit operator JsonNode?(sbyte? value);
public static implicit operator JsonNode?(float? value);
public static implicit operator JsonNode?(string? value);
[System.CLSCompliantAttribute(false)]
public static implicit operator JsonNode?(ushort? value);
[System.CLSCompliantAttribute(false)]
public static implicit operator JsonNode?(uint? value);
[System.CLSCompliantAttribute(false)]
public static implicit operator JsonNode?(ulong? value);
public sealed class JsonArray : JsonNode, IList<JsonNode?>
{
// JsonNodeOptions in the constructors below allow for case-insensitive property names and
// are normally applied only to root nodes since a child node will use the Root node's options.
public JsonArray(JsonNodeOptions? options = null);
// Param-based constructors to support constructor initializers:
public JsonArray(params JsonNode[] items);
public JsonArray(JsonNodeOptions options, params JsonNode[] items);
public static JsonArray Create(JsonElement element, JsonNodeOptions options = default);
public override void WriteTo(Utf8JsonWriter writer, JsonSerializerOptions? options = null);
// When a value can't be implicitly converted to JsonValue, here's a helper that
// allows "Add(value)" instead of the more verbose "Add(JsonValue.Create(value))".
public void Add<T>(T value);
// IList<JsonNode?> (some hidden via explicit implementation):
public int Count { get ;}
bool ICollection<JsonNode?>.IsReadOnly { get ;}
public void Add(JsonNode? item);
public void Clear();
public bool Contains(JsonNode? item);
public IEnumerator<JsonNode?> GetEnumerator();
public int IndexOf(JsonNode? item);
public void Insert(int index, JsonNode? item);
public bool Remove(JsonNode? item);
public void RemoveAt(int index);
void ICollection<JsonNode?>.CopyTo(JsonNode?[]? array, int arrayIndex);
IEnumerator IEnumerable.GetEnumerator();
}
public sealed class JsonObject : JsonNode, IDictionary<string, JsonNode?>
{
// JsonNodeOptions in the constructors below allow for case-insensitive property names and
// are normally applied only to root nodes since a child node will use the Root node's options.
public JsonObject(JsonNodeOptions? options = null);
public static JsonObject Create(JsonElement element, JsonNodeOptions options = default);
public bool TryGetPropertyValue(string propertyName, outJsonNode? jsonNode);
public override void WriteTo(Utf8JsonWriter writer, JsonSerializerOptions? options = null);
// IDictionary<string, JsonNode?> (some hidden via explicit implementation):
public int Count { get; }
bool ICollection<KeyValuePair<string,JsonNode?>>.IsReadOnly { get; }
ICollection<string> IDictionary<string,JsonNode?>.Keys { get; }
ICollection<JsonNode?> IDictionary<string,JsonNode?>.Values { get; }
public void Add(string propertyName,JsonNode? value);
public void Clear();
public bool ContainsKey(string propertyName);
public IEnumerator<KeyValuePair<string,JsonNode?>> GetEnumerator();
public bool Remove(string propertyName);
void ICollection<KeyValuePair<string,JsonNode?>>.Add(KeyValuePair<string,JsonNode> item);
bool ICollection<KeyValuePair<string,JsonNode?>>.Contains(KeyValuePair<string,JsonNode> item);
void ICollection<KeyValuePair<string,JsonNode?>>.CopyTo(KeyValuePair<string,JsonNode>[] array, int arrayIndex);
bool ICollection<KeyValuePair<string,JsonNode?>>.Remove(KeyValuePair<string,JsonNode> item);
IEnumerator IEnumerable.GetEnumerator();
bool IDictionary<string,JsonNode?>.TryGetValue(string propertyName, outJsonNode? jsonNode);
}
public abstract class JsonValue : JsonNode
{
// Prevent external derived classes:
private protected JsonValue(JsonNodeOptions? options = null);
public abstract TValue? GetValue<TValue>(JsonSerializerOptions? options = null);
public abstract bool TryGetValue<TValue>(
out TValue? value,
JsonSerializerOptions? options = null);
// Factory and deserialize methods below that doen't require specifying <T> due to generic type inference. This is necessary for anonymous types.
// T is normally a primitive but can also be JsonElement or any other type.
public static JsonValue Create<T>(T value, JsonNodeOptions? options = null);
}
// Note there is an internal sealed JsonValue<T> class that derives from JsonValue.
public struct JsonNodeOptions
{
public bool PropertyNameCaseInsensitive { get; set; }
// Possibly add later:
// DuplicatePropertyNameHandling { get; set; }
}
}
Currently (and going back to 3.0) a JsonElement
instance is created in three cases by the serializer:
- When a CLR property\field is of type
JsonElement
. - When a CLR property\field is of type
System.Object
. - When a property exists in JSON but does not map to any CLR property. Currently this is stored in a dictionary-backed property (e.g.
IDictionary<string, JsonElement>
) with the[JsonExtensionData]
attribute.
However, using the node classes instead of JsonElement
for the last two cases may be convenient since they are editable plus easier to use. So JsonNode
-derived classes would be returned for System.Object
and JsonObect
for extension data.
namespace System.Text.Json
{
// Determines the type to create for properties and members declared as System.Object.
public enum JsonUnknownTypeHandling
{
JsonElement = 0, // Default
JsonNode = 1, // Create JsonNode*-derived types for System.Object properties and elements.
}
public partial class JsonSerializerOptions
{
public JsonUnknownTypeHandling UnknownTypeHandling {get; set;}
}
}
Sample code to enable:
var options = new JsonSerializerOptions();
options.UnknownTypeHandling = JsonUnknownTypeHandling.JsonNode;
object obj = JsonSerializer.Deserialize<object>("{}", options);
Debug.Assert(obj.GetType() == typeof(JsonObject));
Currently the extension property can be declared in several ways:
// Existing support:
[JsonExtensionData] public Dictionary<string, object?> ExtensionData {get; set;}
[JsonExtensionData] public IDictionary<string, object?> ExtensionData {get; set;}
[JsonExtensionData] public Dictionary<string, JsonElement> ExtensionData {get; set;}
[JsonExtensionData] public IDictionary<string, JsonElement> ExtensionData {get; set;}
When options.UnknownTypeHandling == JsonUnknownTypeHandling.JsonNode
the object?
dictionary value above will create JsonNode?
instances instead of JsonElement
instances. This is for consistency since the type is object
and other usages of object
elsewhere will do the same. However, JsonObject
can be specified as the property type, which is a better experience:
// New support:
[JsonExtensionData] public JsonObject ExtensionData {get; set;}
These and other unnecessary permutations are not supported:
// Not supported:
[JsonExtensionData] public IDictionary<string, JsonNode?> ExtensionData {get; set;}
[JsonExtensionData] public Dictionary<string, JsonNode?> ExtensionData {get; set;}
public partial class JsonSerializer
{
public override TValue GetValue<TValue>(JsonSerializerOptions options = null);
public override TValue GetValue<TValue>(JsonSerializerOptions options = null)
}
These serializer features are supported via JsonSerializerOptions
:
Converters.Add()
(custom converters added at run-time)NumberHandling
DefaultIgnoreCondition
(default\null handling)
The JsonSerializerOptions
are only used during serialization, not deserialization\Parse(). Deserialization uses JsonNodeOptions
and JsonDocumentOptions
instead which makes it clear what is and what is not supported, and doesn't couple deserialization of JsonNode
to the serializer.
A deserialized JsonNode
that is not modified will be re-serialized using the existing JsonElement
semantics where essentially the raw UTF-8 is written back. This is important to call out because the above serializer features do not work during serialization of a JsonElement
. This is a performance optimization that avoids having to expand the entire tree for cases when the JsonSerializerOptions
supports the above options. However, normally POCOs will round-trip the same JSON anyway, so in most cases not supporting these serializer features should be fine.
This method has 4 stages:
Stage 1: return internal value directly from JsonValue
If the <TypeToReturn>
in GetValue<TypeToReturn>
is the same type as <T>
in the internal concrete JsonValue<T>
class then return the internal value:
var jValue = JsonValue.Create(42);
int i = jValue.GetValue<int>(); // returns the internal <T> value
The implementation assumes a direct Type match, not IsAssignableFrom()
semantics.
Stage 2: JsonElement support for known types
For JsonValue<JsonElement>
special logic exists to obtain the known primitives:
JsonNode jObject = JsonNode.Parse(...);
JsonNode jNode = jObject["MyStringProperty"];
string s = jNode.GetValue<string>(); // calls JsonElement.GetString()
This is necessary because the serializer doesn't currently support deserializing values from a JsonElement
instance.
Stage 3: JsonElement support for custom types
Update: not yet implemented pending feedback
If the type is not known by JsonElement
, the raw Utf-8 bytes are obtained, the converter obtained for the type, and converter.TryParse(ReadOnlySpan<byte> utf8Bytes, out T? value, options)
is used.
Stage 4: serializer fallback
Update: not yet implemented pending feedback
Use the serializer to obtain the value. This stage is expensive compared to the other two.
Note that a custom JsonNode
-based converter can be specified to override the built-in custom converters although this should be a rare case.
A JsonNode
is normally serialized by the ToJsonString()
helper method:
JsonObject jObject = ...
// short version:
string json = jObject.ToJsonString();
// equivalent longer versions; note the need to pass in Options (otherwise the default options will be used)
string json = JsonSerializer.Serialize(jObject, jObject.GetType(), jObject.Options);
string json = JsonSerializer.Serialize<JsonObject>(jObject, jObject.Options);
string json = JsonSerializer.Serialize<JsonNode>(jObject, jObject.Options);
For deserializing JSON to a JsonNode
, a static "Parse()" method on JsonNode
and all derived node types is provided:
// short version:
JsonNode jNode = JsonNode.Parse(json, options);
// equivalent longer version:
JsonNode jNode = JsonSerializer.Deserialize<JsonNode>(json, options);
To deserialize a node into a CLR object such as a collection, POCO other value type:
// short version:
MyPoco? obj = jNode.GetValue<MyPoco>();
// equivalent longer version:
MyPoco? obj = JsonSerializer.Deserialize<MyPoco>(jNode.ToJsonString());
The JsonNode
-derived classes have a constructor overload that take a JsonElement
. The resulting node behavior is the same as what would occur if a JsonNode.Parse()
method was called to create the node. This supports:
- A node starting at an arbitrary location in the JSON
- Allows for using
JsonDocument
andJsonElement
obtained earlier. Of interest isJsonDocument
with itsIDisposable
support that uses a pooled alloc for the JSON.
Note that earlier prototypes supported a reverse mode where a JsonElement
can be obtained from a node and that subsequent element would then forward all methods to the JsonNode
(e.g. GetInt32()
, EnumerateObject()
, EnumerateArray()
, etc) including any child elements. This functionality was removed since:
- The main driving scenario was "code that processes read-only JsonElements may want to be invoked with JsonNodes" which does not appear to be that compelling. Another scenario was for "discoverability" since the nice "JsonDocument" term was already used, and being able to navigate from document\element to a node may help with usability.
- The functionality can added later if necessary.
- There is a small perf hit for
JsonElement
since every method will not have an additionalif
statement to detect the mode (readonly normal mode, or writable mode that forwards to nodes). - The layering isn't quite right. A lower-level abstraction (element) would be wrapping and forwarding to a higher-level abstraction (node).
- Unresolved API and usability issues:
- How to do you get a
JsonElement
node that is linked toJsonNode
?- If a
JsonElement.ToNode()
method is added, that can be confusing since a newJsonElement
will need to be created (elements are value types) and then there is not a way to easily get that element back. If we add aJsonNode.AsElement()
method to get that element back, that also be confusing when the node was created with aJsonElement
(through aJsonNode.Parse()
or a node-based constructor that takesJsonElement
since in that case theJsonElement
is not linked to the node. - If a
JsonNode.ToElement()
method is added, that will create a newJsonElement
that references theJsonNode
. This is likely a better experience thanJsonElement.ToNode()
. A correspondingJsonElement.IsNode
andJsonElement.AsNode()
will likely need to be added as well to complement the experience.AsNode()
would throw ifIsNode
isfalse
.
- If a
- The
JsonElement.Clone()
would throwNotSupportedException
since it is not possible to do a deep clone on a node since aJsonValue
can reference an arbitrary CLR object. An alternative is to support a shallow clone for those cases. ToString()
semantics may not be the same.JsonElement.TokenType
would always returnNone
for a linked element since it can't be known forJsonValue
.
- How to do you get a
The JSON primitives (number, string, true, false) do not have their own JsonNode
-derived type like JsonNumber
, JsonString
and JsonBoolean
. Instead, a common JsonValue
class represents them. This allows a JSON number, for example, to serialize and deserialize as either a CLR String
or a Double
depending on usage and options.
Although explicit or implicit cast operators are not required for functionality, based on usability feedback requesting a terse syntax, the use of both explicit cast operators (from a JsonValue
to known primitives) and implicit cast operators (from known primitives to a JsonValue
) are supported.
Note that implicit cast operators do not work in all languages including F#.
Explicit (no operators):
var jArray = new JsonArray();
jArray.Add(JsonValue.Create("hello"));
jArray.Add(new JsonElement<MyCustomDataType>(myCustomDataType));
string s = jArray[0].GetValue<string>();
MyCustomDataType m = jArray[1].GetValue<MyCustomDataType>();
with operators:
var jArray = new JsonArray();
jArray.Add("hello"); // Uses implicit operator.
jArray.Add(new JsonElement<MyCustomDataType>(myCustomDataType)); // No implicit operator for custom types (unless it adds its own)
string s = (string)jArray[0]; // Uses explicit operator.
MyCustomDataType m = jArray[1].GetValue<MyCustomDataType>(options); // no explicit operator for custom types (unless it adds its own). Must pass options that contains custom converter.
with dynamic:
dynamic jArray = new JsonArray();
jArray.Add("hello"); // Uses implicit operator.
jArray.Add(myCustomDataType); // Possible through dynamic.
string s = jArray[0]; // Possible through dynamic.
MyCustomDataType m = jArray[1].GetValue(options); // Must call GetValue(options) to specify custom converter
The JsonNodeOptions
instance needs to be specified in order for options including case sensitivity of property names. Since the explicit and implicit cast operators do not support passing the the options, having them available in the root node is necessary.
It is cumbersome and error-prone to specify the options instance for every new element or property. Consider the verbose syntax:
// Verbose syntax (not recommended, but still supported)
JsonNodeOptions nodeOptions = ...
var jObject = new JsonObject(nodeOptions)
{
["MyString"] = JsonValue.Create("Hello!", nodeOptions),
["MyBoolean"] = JsonValue.Create(false, nodeOptions),
["MyArray"] = new JsonArray(nodeOptions)
{
JsonValue.Create(2, nodeOptions),
JsonValue.Create(3, nodeOptions),
JsonValue.Create(42, nodeOptions)
},
}
and the terse syntax which omits the options instance for non-root members:
// Terse syntax
var jObject = new JsonObject(nodeOptions) // options can just be at root
{
["MyString"] = JsonValue.Create("Hello!"),
["MyBoolean"] = JsonValue.Create(false),
["MyArray"] = new JsonArray()
{
JsonValue.Create(2),
JsonValue.Create(3),
JsonValue.Create(42)
},
}
including implicit operators and JsonArray
support for params
:
var jObject = new JsonObject(nodeOptions) // options can just be at root
{
["MyString"] = "Hello!",
["MyCustomDataType"] = false,
["MyArray"] = new JsonArray(2, 3, 42)
}
Nodes only allow the JsonNodeOptions
value to be specified during creation or during deserialization, and not during serialization since they are not used. Instead, JsonSerializerOptions
are used during serialization.
Child nodes use the parent JsonNodeOptions
, recursively, if they don't have any options specified during creation.
JsonNode
has indexers that allow JsonObject
properties and JsonArray
elements to be specified. This allows a terse mode:
// Terse programming model:
string str = jNode["Child"]["Array"][0]["Message"].GetValue<string>();
If System.Object
was used instead of JsonNode
, or the indexers were not exposed on JsonNode
, the above terse syntax wouldn't work and a more verbose syntax would be necessary:
// Programming model not acceptable:
string str = ((JsonValue)((JsonObject)((JsonArray)((JsonObject)((JsonObject)jObject)["Child"])["Array"])[0])["Message"]).GetValue<string>();
The indexer for JsonObject
returns null
for missing properties. This aligns with:
- Expected support for
dynamic
. - Newtonsoft.
- The serializer today. This includes when deserializing JSON to
System.Object
,null
is returned today, not, for example, aJsonElement
withValueKind == JsonValueKind.Null
.
However, for some scenarios, it is important to distinguish between a null
value deserialized from JSON vs. a missing property. Since JsonObject
implements IDictionary<string, JsonNode>
it can be inspected:
bool found = jObject.TryGetValue("NonExistingProperty", out object _);
Although JsonValue
is intended to support simple value types, any CLR value including POCOs and various collection types can be specified, assuming they are supported by the serializer. If they are not supported by the serializer, an exception or incorrect serialization can occur.
However, normally one would use JsonArray
or JsonObject
instead to create a new POCO or collection. A POCO or collection in this scenario is serialized as expected (as a JSON object or array) but when deserialized the type will be either a JsonObject
or a JsonArray
, not a JsonValue
.
One reason to support this (and not throw) is because it is not possible to identify what comprises a POCO (JsonObject
) vs. a value from the <T>
in JsonValue<T>
. However, an array (JsonArray
) can be identified somewhat reliabily since <T>
will implement IEnumerable
.
Unless we remove the support for custom data types, allowing any type of object in JsonValue
is assumed.
var jObject = new JsonObject()
{
["MyString"] = "Hello!",
["MyNull"] = null,
["MyBoolean"] = false,
["MyInt"] = 43,
["MyDateTime"] = new DateTime(2020, 7, 8),
["MyGuid"] = new Guid("ed957609-cdfe-412f-88c1-02daca1b4f51"),
["MyArray"] = new JsonArray(2, 3, 42),
["MyObject"] = new JsonObject()
{
["MyString"] = "Hello!!"
},
["Child"] = new JsonObject()
{
["ChildProp"] = 1
}
};
Currently dynamic
doesn't support object initializers, so a more verbose model is required:
dynamic jObject = new JsonObject();
jObject.MyString = "Hello!";
jObject.MyNull = null;
jObject.MyBoolean = false;
jObject.MyInt = 43;
jObject.MyDateTime = new DateTime(2020, 7, 8);
jObject.MyGuid = new Guid("ed957609-cdfe-412f-88c1-02daca1b4f51");
jObject.MyArray = new JsonArray(2, 3, 42);
jObject.MyObject = new JsonObject();
// We call .MyObject again, but could manually reference the previous value for perf.
jObject.MyObject.MyString = "Hello!!"
jObject.MyObject.Child = new JsonObject();
jObject.MyObject.Child.ChildProp = 1;
JsonObject
and JsonArray
implement IEnumerable
which is the basic for LINQ. In addition, the JsonNode.Parent
and JsonNode.Root
properties can be used to help query parent:child relationships.
private class BlogPost
{
public string Title { get; set; }
public string AuthorName { get; set; }
public string AuthorTwitter { get; set; }
public string Body { get; set; }
public DateTime PostedDate { get; set; }
}
{
string json = @"
[
{
""Title"": ""TITLE."",
""Author"":
{
""Name"": ""NAME."",
""Mail"": ""MAIL."",
""Picture"": ""/PICTURE.png""
},
""Date"": ""2021-01-20T19:30:00"",
""BodyHtml"": ""Content.""
}
]";
JsonArray arr = (JsonArray)JsonNode.Parse(json);
// Convert nested JSON to a flat POCO.
IList<BlogPost> blogPosts = arr.Select(p => new BlogPost
{
Title = p["Title"].GetValue<string>(),
AuthorName = p["Author"]["Name"].GetValue<string>(),
AuthorTwitter = p["Author"]["Mail"].GetValue<string>(),
PostedDate = p["Date"].GetValue<DateTime>(),
Body = p["BodyHtml"].GetValue<string>()
}).ToList();
const string expected = "[{\"Title\":\"TITLE.\",\"AuthorName\":\"NAME.\",\"AuthorTwitter\":\"MAIL.\",\"Body\":\"Content.\",\"PostedDate\":\"2021-01-20T19:30:00\"}]";
string json_out = JsonSerializer.Serialize(blogPosts);
Debug.Assert(expected == json_out);
}
const string Linq_Query_Json = @"
[
{
""OrderId"":100, ""Customer"":
{
""Name"":""Customer1"",
""City"":""Fargo""
}
},
{
""OrderId"":200, ""Customer"":
{
""Name"":""Customer2"",
""City"":""Redmond""
}
},
{
""OrderId"":300, ""Customer"":
{
""Name"":""Customer3"",
""City"":""Fargo""
}
}
]";
{
// Query for orders
JsonArray allOrders = JsonNode.Parse(Linq_Query_Json).AsJsonArray();
IEnumerable<JsonNode> orders = allOrders.Where(o => o["Customer"]["City"].GetValue<string>() == "Fargo");
Debug.Assert(2 == orders.Count());
Debug.Assert(100 == orders.ElementAt(0)["OrderId"].GetValue<int>());
Debug.Assert(300 == orders.ElementAt(1)["OrderId"].GetValue<int>());
Debug.Assert("Customer1" == orders.ElementAt(0)["Customer"]["Name"].GetValue<string>());
Debug.Assert("Customer3" == orders.ElementAt(1)["Customer"]["Name"].GetValue<string>());
}
{
// Query for orders with dynamic
IEnumerable<dynamic> allOrders = (IEnumerable<dynamic>)JsonNode.Parse(Linq_Query_Json);
IEnumerable<dynamic> orders = allOrders.Where(o => ((string)o.Customer.City) == "Fargo");
Debug.Assert(2 == orders.Count());
Debug.Assert(100 == (int)orders.ElementAt(0).OrderId);
Debug.Assert(300 == (int)orders.ElementAt(1).OrderId);
Debug.Assert("Customer1" == (string)orders.ElementAt(0).Customer.Name);
Debug.Assert("Customer3" == (string)orders.ElementAt(1).Customer.Name);
}
The dynamic feature in C# is implemented in the CLR by both language features and in the System.Linq.Expressions
assembly. The core interface is IDynamicMetaObjectProvider.
There are existing implementations of IDynamicMetaObjectProvider
including DynamicObject and ExpandObject. However, these implementations are not ideal for JSON scenarios because:
- There are many public members used for wiring up dynamic support, but are of little to no value for consumers thus they would be confusing when mixed in with other methods intended to be used for writable DOM support.
ExpandoObject
does not support case insensitivity and throws an exception when accessing missing properties (anull
is desired instead).DynamicObject
prevents potential optimizations including property-lookup and avoiding some reflection calls.
Thus, the design presented here for dynamic
assumes explicit interface implementation of IDynamicMetaObjectProvider
. This is also what Newtonsoft does; its JToken implements IDynamicMetaObjectProvider
.
Having System.Text.Json.dll
directly reference System.Linq.Expressions.dll
is feasible because the ILLinker is able to remove the reference to the large System.Linq.Expressions.dll
when the dynamic functionality is not used. In tests, a simple JsonNode
stand-alone console app not using dynamic
was ~10.5MB and using dynamic grew to ~11.5MB. It was also verified the SLE assembly reference in STJ was removed when not using dynamic
.
For a JsonObject
, this programming model:
dynamic obj = JsonNode.Parse<object>("{\"MyProp\":42}", options);
is equivalent to
dynamic obj = JsonNode.Parse<dynamic>("{\"MyProp\":42}", options);
and
dynamic obj = JsonNode.Parse<JsonNode>("{\"MyProp\":42}", options);
dynamic obj = JsonNode.Parse<JsonObject>("{\"MyProp\":42}", options);
Here's an example using a custom converter along with dynamic to populate a POCO.
internal class PersonConverter : JsonConverter<Person>
{
public override Person Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
dynamic jObject = JsonNode.Parse<object>(ref reader, options);
// Pass values into the constructor which requires the values to be read ahead of time.
Person person = new Person(jObject.Id)
{
Name = jObject.name,
AddressLine1 = jObject.addr1,
AddressLine2 = jObject.addr2,
City = jObject.city,
State = jObject.state,
Zip = jObject.zip
}
return person;
}
public override void Write(Utf8JsonWriter writer, Person value, JsonSerializerOptions options)
{
// Standard serialization is fine in this example.
JsonSerializer.Serialize<Person>(writer, value, options);
}
}
JsonElement
is used as the internal deserialized value. Since it supports lazy creation of values, it is performant for scenarios that don't fully access each property or element. For example, the contents of a JsonValue<string>
internally is a UTF-8 Span<byte>
, not a string
, is not "cracked" open until the value is requested.
Note that JsonElement
is now ~2x faster in 6.0 for cases used here.
The design supports lazy and shallow creation which is performant when only a subset of the tree is accessed.
A JsonNode
tree is populated when JsonObject
and JsonArray
instances are navigated. For example, a JsonObject
contains only its internal JsonElement
value after deserialization. When a property is accessed for the first time, a JsonNode
instance is created for every property and added to the JsonObject
's internal dictionary. Each of those child nodes maintains a single reference to its corresponding JsonElement
which internally still points to the shared UTF-8 buffer on the parent JsonDocument
.
The design of values uses generics to hold internal <T>
value. This avoids boxing for value types. A given Value<T>
, such as Value<int>
can be modified by the Value
property which can be used to avoid creation of a new JsonValue
instance.
The property-lookup algorithm can be made more efficient than the standard dictionary by using ordering heuristics (like the current serializer) or a bucketless B-Tree with no need to call Equals()
. The Azure dynamic
prototyping effort has been thinking of the B-Tree approach.
A [DebuggerDisplay]
attribute will be added to JsonNode
that can performs a pretty-printed version of the JSON via ToJsonString()
.
When JsonValue.ToJsonString()
is called for primitives such as for an Int32
that are not backed by a JsonElement
, the simplest implementation would be:
public string ToJsonString()
{
return JsonSerializer.Serialize<T>(this.Value, this.Options);
}
However this involves many steps that make this relatively slow:
- Obtaining the converter for
<T>
. - Creating an instance of a
Utf8JsonWriter
. - Allocating a temporary buffer (from a pool).
- Calling the custom converter, which calls the writer, which writes to the buffer.
- Transcoding the UTF8-based buffer to a string.
One likely optimization is to cache the common known converters on JsonSerializerOptions
and look them up from the type (e.g. Int32
). Once found, the converter can also be cached on the JsonValue
instance for future calls to the same instance.
However, for the case where only one value is serialized (and not a whole tree) this is still going to be slower than a design that can quickly return a string from a value without getting the writer and serializer involved. Such a design would add overloads including a new string-based overload that can also used to extended the existing quoted number handling to custom converers (currently quoted numbers support isn't extensible and only works on known types).
These new overloads will be implemented on the existing converters:
namespace System.Text.Json.Serialization
{
public partial class JsonConverter<T>
{
// Override for fast JsonValue.ToJsonString() and to support quoted number extensibility.
public virtual bool TryConvert(T? value, out string? value, JsonSerializerOptions? options)
{
// Default implementation
value = null;
return false;
}
// Used with JsonNode.ToUtf8Bytes().
public virtual bool TryConvert(T? value, out byte[]? value, JsonSerializerOptions? options)
{
// Default implementation
value = null;
return false;
}
// Used with JsonValue.GetValue() when backed by JsonElement for unknown types.
public virtual bool TryParse(ReadOnlySpan<byte> utf8Bytes, out T? value, JsonSerializerOptions? options)
{
// Default implementation
value = null;
return false;
}
}
}
If false
is returned, the slow serializer path is used.
These feature are reasonable, but not proposed (at least for the first round):
- Support for
JsonObject.AddBefore()
andAddAfter()
for exact placement of new properties. - Reading\writing JSON comments. Not implemented since
JsonDocument
doesn't support it, although the reader and writer support them.- Cost to implement is not known (no known prototype or PR).
- For the public API, a
JsonValueKind.Comment
enum value would be added and then a mechanism such as a ValueKind property added toJsonNode
.
IEqualityComparer<T>
implementation such as aJsonNodeComparer
class. This would imply a deep compare which is expensive, although that would be expected.- "JsonPath" query support. Newtonsoft has this as a way to parse a subset of JSON into a
JToken
. - Annotations. Newtonsoft has this to provide LineNumber and Position and user-specified values.
JsonNode.LineNumber
andPosition
properties. Although the properties don't exist, any inner reader exception orJsonException
during deserialization will have that information. Note that aJsonNodePath
property, however, is supported including determining the path at runtime even after modifications to a property name, element ordering, etc.- Non-generic overloads on
JsonNode
. Note that if implemented they will not be fast since the internal representation is a generic<T>
value, which requires boxing and\or a indirect call to the generic method:public virtual object GetArrayElement(Type type, int index);
public virtual object GetPropertyValue(Type type, string propertyName);
public abstract object GetValue(Type type);
public abstract bool TryGetValue(Type type, out object? value);
- More interop with JsonElement. See "Interop with JsonElement".
These features will likely never be implemented:
- Support for
System.ComponentModel.TypeConverter
in theGetValue<TypeToReturn>()
method or another new method. Currently the serializer does not support this either. - Support for reference handling because:
- Performance. When reference handling is enabled, all nodes must be materialized eagerly.
- Likely not expected since a DOM is lower-level than an object tree. Consumers would expect the metadata to be available to them programmatically, not hidden (although it could be an option).
Equals()
,==
and!=
operators. These would be slow since they imply a deep compare in non-trivial cases. TheIEqualityComparer<T>
mechanism is more suitable. It would be useful for those with a custom dictionary or similar cases; it would likely use the case senstivity options for comparing property names.
Other issues to consider along with this:
- Api proposal: Change JsonSerializerOptions default settings
- Useful to prevent the
JsonSerializerOption
parameter from having to be specified.
- Useful to prevent the
- We should be able serialize and deserialize from DOM
- For consistency with
JsonNode
.
- For consistency with
- More extensible object and collection converters
- The DOM could be used in a future feature to make object and collection custom converters easier to use.
From Workspaces - Create Or Update.
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Node;
using Microsoft.Azure.Management.Synapse.Models;
namespace CallingAzureApi
{
class Program
{
static void Main(string[] args)
{
var request = new JsonObject
{
["identity"] = new JsonObject
{
["type"] = "SystemAssigned"
},
["FOOOO"] = null,
["properties"] = new JsonObject
{
["FOOOO"] = null,
["defaultDataLakeStorage"] = new JsonObject
{
["accountUrl"] = "https://accountname.dfs.core.windows.net",
["filesystem"] = "default"
},
["managedVirtualNetworkSettings"] = new JsonObject
{
["preventDataExfiltration"] = false,
["linkedAccessCheckOnTargetResource"] = false,
["allowedAadTenantIdsForLinking"] = new JsonArray { "740239CE-A25B-485B-86A0-262F29F6EBDB" }
},
["purviewConfiguration"] = new JsonObject
{
["purviewResourceId"] = "/subscriptions/00000000-1111-2222-3333-444444444444/resourceGroups/resourceGroup1/providers/Microsoft.ProjectPurview/accounts/accountname1"
},
["sqlAdministratorLogin"] = "login",
["sqlAdministratorLoginPassword"] = "password",
["managedVirtualNetwork"] = "default",
["managedResourceGroupName"] = "workspaceManagedResourceGroupUnique",
["workspaceRepositoryConfiguration"] = new JsonObject
{
["type"] = "FactoryGitHubConfiguration",
["hostName"] = string.Empty,
["accountName"] = "mygithubaccount",
["projectName"] = "myProject",
["repositoryName"] = "myRepository",
["collaborationBranch"] = "master",
["rootFolder"] = "/",
},
["encryption"] = new JsonObject
{
["cmk"] = new JsonObject
{
["key"] = new JsonObject
{
["name"] = "default",
["keyVaultUrl"] = "https://vault.azure.net/keys/key1"
}
}
}
},
["location"] = "East US",
["tags"] = new JsonObject
{
["key"] = "value"
}
};
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);
options.WriteIndented = true;
string json = request.ToJsonString(options);
Console.WriteLine(json);
}
}
}
Note that collection initializers can also be used instead of indexers, although the additional braces seem to get in the way a bit:
var request = new JsonObject
{
{"identity", new JsonObject {
{ "type", "SystemAssigned" }
} },
{ "properties", new JsonObject {
{ "defaultDataLakeStorage", new JsonObject {
{ "accountUrl", "https://accountname.dfs.core.windows.net" },
{ "filesystem", "default" }
} }
} }
// continue this pattern
};
Finally the example could be modified to use a hybrid of anonymous types and Azure named types:
var request = JsonValue.Create(new // an anonymous type is used to hold top-level properties
{
Identity = new ManagedIdentity
{
Type = ResourceIdentityType.SystemAssigned
},
Properties = new Workspace
{
DefaultDataLakeStorage = new DataLakeStorageAccountDetails
{
AccountUrl = "https://accountname.dfs.core.windows.net",
Filesystem = "default"
},
ManagedVirtualNetworkSettings = new ManagedVirtualNetworkSettings
{
PreventDataExfiltration = false,
LinkedAccessCheckOnTargetResource = false,
AllowedAadTenantIdsForLinking = new List<string> { "740239CE-A25B-485B-86A0-262F29F6EBDB" }
},
PurviewConfiguration = new PurviewConfiguration("/subscriptions/00000000-1111-2222-3333-444444444444/resourceGroups/resourceGroup1/providers/Microsoft.ProjectPurview/accounts/accountname1"),
SqlAdministratorLogin = "login",
SqlAdministratorLoginPassword = "password",
ManagedVirtualNetwork = "default",
ManagedResourceGroupName = "workspaceManagedResourceGroupUnique",
WorkspaceRepositoryConfiguration = new WorkspaceRepositoryConfiguration
{
Type = "FactoryGitHubConfiguration",
HostName = string.Empty,
AccountName = "mygithubaccount",
ProjectName = "myProject",
RepositoryName = "myRepository",
CollaborationBranch = "master",
RootFolder = "/"
},
Encryption = new EncryptionDetails
{
Cmk = new CustomerManagedKeyDetails
{
Key = new WorkspaceKeyDetails
{
Name = "default",
KeyVaultUrl = "https://vault.azure.net/keys/key1"
}
}
}
},
Location = "East US",
Tags = new JsonObject // Use JsonObject for "Tags" (e.g. may not be possible to use anonymous types for this if values are assigned at run-time)
{
["key"] = "value"
}
});
Note the JSON will be ordered slightly differently due to the serializer. Also additional JsonSerializerOptions
need to be used to support writing enum values and ignoring nulls:
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);
options.WriteIndented = true;
// Support the serializer better
options.Converters.Add(new JsonStringEnumConverter());
options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
string json = request.ToJsonString(options);
Console.WriteLine(json);
- Review general direction of API. Primarily using the serializer methods vs. new
node.Parse()
\node.Write()
methods and having a sealedJsonValue
class instead of separate number, string and boolean classes. - Update API based on feedback and additional prototyping.
- Provide more samples (LINQ).
- Create API issue.
- Approve API.
- Prototype and usabilty studies (likely in collaboration with the Azure SDK team).
- Modify API as necessary based on usability.
- Ensure any future "new dynamic" C# support (being considered for intellisense support based on schema) is forward-compatible with the work here which uses the "old dynamic".