Your Note class needs a parameterless constructor

class Note
{
    public DateTime currentDate { get; set; }
    public string summary { get; set; }

    // add this
    public Note()
    {
    }

    public Note(DateTime _date, string _sum)
    {
        currentDate = _date;
        summary = _sum;
    }
}

It might be worth thinking if you need your original two parameter constructor. If you removed it, then you could instantiate a new Note like this

var completeNote = new Note
{
    currentdate = date,
    summary = givenNote
};
Answer from Kevin Brydon on Stack Overflow
🌐
Microsoft Learn
learn.microsoft.com › en-us › dotnet › standard › serialization › system-text-json › deserialization
How to deserialize JSON in C# - .NET | Microsoft Learn
A common way to deserialize JSON is to have (or create) a .NET class with properties and fields that represent one or more of the JSON properties. Then, to deserialize from a string or a file, call the JsonSerializer.Deserialize method.
Discussions

System.Text.Json failure to deserialize strongly typed models from string
System.Text.Json fails to deserialize all properties under all circumstances for all models. It cannot deserialize anything into an object from string, stream, or otherwise. Newtonsoft works as expected -> reads json string and produces a list of objects set by the json properties. System does not work, it always returns null/default/0/Empty values. This is true ... More on github.com
🌐 github.com
8
July 1, 2022
System.Text.Json fails to deserialize into a model with an ImmutableList property.
Description The StackOverflow issue https://stackoverflow.com/questions/72268018/jsonconstructor-fails-on-ienumerable-property describes a scenario trying to deserialize an object that has an immutable collection as a property using the ... More on github.com
🌐 github.com
3
May 17, 2022
c# - System.Text.Json.deserialize Json String - Stack Overflow
Find centralized, trusted content and collaborate around the technologies you use most. Learn more about Collectives ... Bring the best of human thought and AI automation together at your work. Explore Stack Internal ... I have a question about System.Text.Json.deserialize. More on stackoverflow.com
🌐 stackoverflow.com
c# - Deserialize json in a "TryParse" way - Stack Overflow
You already deserialized once to JObject, so it seems like a cast might be cheaper? I really don't know. 2017-03-17T19:17:23.9Z+00:00 ... Also in many cases if you don't control the JSON it might be wise to wrap the JObject.Parse() in a separate method with a separate try/catch since it throws exception for invalid JSON... More on stackoverflow.com
🌐 stackoverflow.com
🌐
Microsoft Learn
learn.microsoft.com › en-us › dotnet › standard › serialization › system-text-json › how-to
How to serialize JSON in C# - .NET | Microsoft Learn
To pretty-print the JSON output, set JsonSerializerOptions.WriteIndented to true: using System.Text.Json; namespace SerializeWriteIndented { public class WeatherForecast { public DateTimeOffset Date { get; set; } public int TemperatureCelsius { get; set; } public string?
🌐
GitHub
github.com › dotnet › aspnetcore › issues › 42545
System.Text.Json failure to deserialize strongly typed models from string · Issue #42545 · dotnet/aspnetcore
July 1, 2022 - System.Text.Json fails to deserialize all properties under all circumstances for all models. It cannot deserialize anything into an object from string, stream, or otherwise. Newtonsoft works as expected -> reads json string and produces a list ...
Author   optimizasean
🌐
Microsoft Learn
learn.microsoft.com › en-us › dotnet › api › system.text.json.jsonserializer.deserialize
JsonSerializer.Deserialize Method (System.Text.Json) | Microsoft Learn
The Stream will be read to completion. public: static System::Object ^ Deserialize(System::IO::Stream ^ utf8Json, System::Text::Json::Serialization::Metadata::JsonTypeInfo ^ jsonTypeInfo);
🌐
GitHub
github.com › dotnet › runtime › issues › 69417
System.Text.Json fails to deserialize into a model with an ImmutableList property. · Issue #69417 · dotnet/runtime
May 17, 2022 - System.Text.Json fails to deserialize into a model with an ImmutableList property.#69417 · Copy link · Labels · area-System.Text.Json · ericrini · opened · on May 17, 2022 · Issue body actions · The StackOverflow issue https://stackoverflow.com/questions/72268018/jsonconstructor-fails-on-ienumerable-property describes a scenario trying to deserialize an object that has an immutable collection as a property using the System.Text.Json serializer.
Published   May 17, 2022
Author   ericrini
Top answer
1 of 3
3

Don't know why you want to use the JsonNode.Parse().

If you just want to modify the location's value, maybe you can use the JsonSerializer with BookStore and Book class to help you.
The Book class is a IEnumerable property of BookStore.

an example by .NET6 Console App
BookClass is a 2-level nested class

// See https://aka.ms/new-console-template for more information
using System.Text.Json;

// Initialize json string
string jsonStr = "{\"id\": 1, \"ShopName\" :\"Paint Shop\", \"Books\" : [\r\n            {\r\n                \"bookId\":1,\r\n                \"bookName\":\"Peter Pan\",\r\n                \"location\":\"A01\"\r\n            },\r\n            {\r\n                \"bookId\":2,\r\n                \"bookName\":\"Cooking Book\",\r\n                \"location\":\"A02\"\r\n            }\r\n        ]\r\n    }";

// Deserilize to object
Bookstore bookstore = JsonSerializer.Deserialize<Bookstore>(jsonStr);

// Write the location's value
Console.WriteLine(bookstore.Books.ToArray()[1].location);

// Modified the location's value
bookstore.Books.ToArray()[1].location += "_Modified";

// Write the modified location's value
Console.WriteLine(bookstore.Books.ToArray()[1].location);

// See result output
Console.ReadLine();

public class Bookstore
{
    public int id { get; set; }
    public string ShopName { get; set; }
    public IEnumerable<Book> Books { get; set; }
}

public class Book
{
    public int bookId { get; set; }
    public string bookName { get; set; }
    public string location { get; set; }
}

Result output image


If you persist to use JsonNode.Parse(), then need more steps. like this.

// See https://aka.ms/new-console-template for more information
using System.Text.Json;
using System.Text.Json.Nodes;

// Initialize json string
string jsonStr = "{\"id\": 1, \"ShopName\" :\"Paint Shop\", \"Books\" : [\r\n            {\r\n                \"bookId\":1,\r\n                \"bookName\":\"Peter Pan\",\r\n                \"location\":\"A01\"\r\n            },\r\n            {\r\n                \"bookId\":2,\r\n                \"bookName\":\"Cooking Book\",\r\n                \"location\":\"A02\"\r\n            }\r\n        ]\r\n    }";

// Parse to jsonNode
JsonNode jsonNode = JsonNode.Parse(jsonStr);

// Convert to JsonObject
JsonObject jsonObject = jsonNode.AsObject();

// Convert Books to Json Array
JsonArray jsonArray = jsonObject["Books"].AsArray();

// Convert index 1 in array to Json object
JsonObject book1Object = jsonArray[1].AsObject();

// Write the location value 
Console.WriteLine(book1Object["location"]);

// Modify location value 
book1Object["location"] += "_modified";

// Write the modified location value 
Console.WriteLine(book1Object["location"]);

// See output
Console.ReadLine();

public class Bookstore
{
    public int id { get; set; }
    public string ShopName { get; set; }
    public IEnumerable<Book> Books { get; set; }
}

public class Book
{
    public int bookId { get; set; }
    public string bookName { get; set; }
    public string location { get; set; }
}


Other suggestion

Use Newtonsoft.Json is better. Because when convert a json to a C# object, will need to convert many properties with different value type. The Newtonsoft.Json had already handle these situation, at the mean time the System.Text.Json does not.

2 of 3
1

You can use JObject to modify json directly

var jObject = JObject.Parse(json);

jObject["Books"][1]["location"] = "new value";

Result:

Note:

You should add Newtonsoft.Json package.

Find elsewhere
🌐
Josef Ottosson
josef.codes › protect-yourself-when-deserializing-system-text-json
Protect yourself when deserializing - System.Text.Json
February 27, 2024 - I've shown two approaches to protect yourselve against "bad" json. It's better to get an exception when trying to deserialize the json compared to getting an obscure null reference exception later on in your code.
🌐
Marc Roussy
marcroussy.com › 2020 › 08 › 17 › deserialization-with-system-text-json
Deserialization with System.Text.Json – Marc Roussy
January 22, 2021 - System.Text.Json adheres to RFC 8259, so if you’re ever left wondering why a setting is different from Newtonsoft, that’s probably why. ... Have a POCO that matches the JSON data, or it’s easy to create one. Need to use most of the properties of the JSON in your application code. JsonDocument.Parse deserializes JSON to its constituent parts and makes it accessible through an object model that can represent any valid JSON.
Top answer
1 of 8
99

@Victor LG's answer using Newtonsoft is close, but it doesn't technically avoid the a catch as the original poster requested. It just moves it elsewhere. Also, though it creates a settings instance to enable catching missing members, those settings aren't passed to the DeserializeObject call so they are actually ignored.

Here's a "catch free" version of his extension method that also includes the missing members flag. The key to avoiding the catch is setting the Error property of the settings object to a lambda which then sets a flag to indicate failure and clears the error so it doesn't cause an exception.

 public static bool TryParseJson<T>(this string @this, out T result)
 {
    bool success = true;
    var settings = new JsonSerializerSettings
    {
        Error = (sender, args) => { success = false; args.ErrorContext.Handled = true; },
        MissingMemberHandling = MissingMemberHandling.Error
    };
    result = JsonConvert.DeserializeObject<T>(@this, settings);
    return success;
}

Here's an example to use it:

if(value.TryParseJson(out MyType result))
{ 
    // Do something with result…
}
2 of 8
64

With Json.NET you can validate your json against a schema:

 string schemaJson = @"{
 'status': {'type': 'string'},
 'error': {'type': 'string'},
 'code': {'type': 'string'}
}";

JsonSchema schema = JsonSchema.Parse(schemaJson);

JObject jobj = JObject.Parse(yourJsonHere);
if (jobj.IsValid(schema))
{
    // Do stuff
}

And then use that inside a TryParse method.

public static T TryParseJson<T>(this string json, string schema) where T : new()
{
    JsonSchema parsedSchema = JsonSchema.Parse(schema);
    JObject jObject = JObject.Parse(json);

    return jObject.IsValid(parsedSchema) ? 
        JsonConvert.DeserializeObject<T>(json) : default(T);
}

Then do:

var myType = myJsonString.TryParseJson<AwsomeType>(schema);

Update:

Please note that schema validation is no longer part of the main Newtonsoft.Json package, you'll need to add the Newtonsoft.Json.Schema package.

Update 2:

As noted in the comments, "JSONSchema" have a pricing model, meaning it isn't free. You can find all the information here

Top answer
1 of 2
10

I've had a look at your fiddle and spotted a couple of problems. Working fiddle here

  1. System.Text.Json is case-sensitive by default (except for web apps). You can resolve this by using either PropertyNamingPolicy = JsonNamingPolicy.CamelCase or PropertyNameCaseInsensitive = true in the serializer options.

  2. The second issue is outlined in Enums as strings

    By default, enums are serialized as numbers. To serialize enum names as strings, use the JsonStringEnumConverter.

    You should add JsonSerializerOptions to resolve (1) and (2):

    var options = new JsonSerializerOptions 
    {
        Converters =
        {
            new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)
        },
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase
    };
    var items = JsonSerializer.Deserialize<List<MenuItem>>(json, options);
    
  3. The third issue appears to be with the binding in the constructor for the list of Permissions. In the constructor you define a List<Permission> for the permissions parameter. I receive the error in your question unless the constructor argument type matches the model property type exactly. So, I updated the constructor to take a IReadOnlyList<Permission> and it deserializes successfully:

    [JsonConstructor]
    public MenuItem
        (
            string name, 
            string url,
            IReadOnlyList<Permission> permissions
        )
    {
        this.Name = name;
        this.Url = url;
        this.Permissions = permissions ?? new List<Permission>().AsReadOnly();
    }
    

    Alternatively, you could change the Permissions property to List<Permission>.

This answer to a question with a similar problem explains that this is actually a limitation of System.Text.Json and there is currently an open github issue.

A working fork of your fiddle is demoed here.

2 of 2
0

I had the same issue. The solution is to use

JsonSerializable

attribute of

System.Text.Json.Serialization

It looks like:

[JsonSerializable(typeof(YourClassName))]
public class YourClassName
{
    public byte YourPropertyName { get; set; }
}

Always class and always public and always properties and always use the attribute on the class.

🌐
GitHub
github.com › dotnet › runtime › issues › 30969
[System.Text.Json] serialize/deserialize any object · Issue #30969 · dotnet/runtime
September 26, 2019 - JsonSerializerSettings ...SerializeObject(value, jsonSerializerSettings); // deserialize object value = JsonConvert.DeserializeObject(json, jsonSerializerSettings);...
Author   MichaelRumpler
Top answer
1 of 6
81
  1. The new System.Text.Json api exposes a JsonConverter api which allows us to convert the type as we like.

    For example, we can create a generic number to string converter:

    public class AutoNumberToStringConverter : JsonConverter<object>
    {
        public override bool CanConvert(Type typeToConvert)
        {
            return typeof(string) == typeToConvert;
        }
        public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            if(reader.TokenType == JsonTokenType.Number) {
                return reader.TryGetInt64(out long l) ?
                    l.ToString():
                    reader.GetDouble().ToString();
            }
            if(reader.TokenType == JsonTokenType.String) {
                return reader.GetString();
            }
            using(JsonDocument document = JsonDocument.ParseValue(ref reader)){
                return document.RootElement.Clone().ToString();
            }
        }
    
        public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
        {
            writer.WriteStringValue( value.ToString());
        }
    }
    
  2. When working with MVC/Razor Page, we can register this converter in startup:

    services.AddControllersWithViews().AddJsonOptions(opts => {
        opts.JsonSerializerOptions.PropertyNameCaseInsensitive= true;
        opts.JsonSerializerOptions.Converters.Insert(0, new AutoNumberToStringConverter());
    });
    

    and then the MVC/Razor will handle the type conversion automatically.

  3. Or if you like to control the serialization/deserialization manually:

    var opts = new JsonSerializerOptions {
        PropertyNameCaseInsensitive = true,
    };
    opts.Converters.Add(new AutoNumberToStringConverter());
    var o = JsonSerializer.Deserialize<Product>(json,opts) ;
    
  4. In a similar way you can enable string to number type conversion as below :

    public class AutoStringToNumberConverter : JsonConverter<object>
    {
        public override bool CanConvert(Type typeToConvert)
        {
            // see https://stackoverflow.com/questions/1749966/c-sharp-how-to-determine-whether-a-type-is-a-number
            switch (Type.GetTypeCode(typeToConvert))
            {
                case TypeCode.Byte:
                case TypeCode.SByte:
                case TypeCode.UInt16:
                case TypeCode.UInt32:
                case TypeCode.UInt64:
                case TypeCode.Int16:
                case TypeCode.Int32:
                case TypeCode.Int64:
                case TypeCode.Decimal:
                case TypeCode.Double:
                case TypeCode.Single:
                return true;
                default:
                return false;
            }
        }
        public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            if(reader.TokenType == JsonTokenType.String) {
                var s = reader.GetString() ;
                return int.TryParse(s,out var i) ? 
                    i :
                    (double.TryParse(s, out var d) ?
                        d :
                        throw new Exception($"unable to parse {s} to number")
                    );
            }
            if(reader.TokenType == JsonTokenType.Number) {
                return reader.TryGetInt64(out long l) ?
                    l:
                    reader.GetDouble();
            }
            using(JsonDocument document = JsonDocument.ParseValue(ref reader)){
                throw new Exception($"unable to parse {document.RootElement.ToString()} to number");
            }
        }
    
    
        public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
        {
            var str = value.ToString();             // I don't want to write int/decimal/double/...  for each case, so I just convert it to string . You might want to replace it with strong type version.
            if(int.TryParse(str, out var i)){
                writer.WriteNumberValue(i);
            }
            else if(double.TryParse(str, out var d)){
                writer.WriteNumberValue(d);
            }
            else{
                throw new Exception($"unable to parse {str} to number");
            }
        }
    }
    
2 of 6
29

If you only want to deserialize strings as numbers, you could simply set the NumberHandling property to AllowReadingFromString in the options:

var o = JsonSerializer.Deserialize<Product>(json, new JsonSerializerOptions
{
    // [...]
    NumberHandling = JsonNumberHandling.AllowReadingFromString
});

Note: This will automatically deserialize strings as numbers, but not numbers as string. If you need more advanced logic, you will need a custom converter (see other answers).

🌐
Reddit
reddit.com › r/dotnet › check if json string can be deserialized into specific class
r/dotnet on Reddit: Check if json string can be deserialized into specific class
February 19, 2024 -

So I have a json string like:

"{\"thisisnorthecorrectname\":\"Value1\"}"

Now I want to use System.Text.Json to check if this string can be deserialized into this class:

internal class Test {
    public string Name { get; set; }
}

I have tried with a method like this:

static bool CanDeserialize<T>(string jsonString, out T deserializedObject) {
    deserializedObject = default;
    try {
        deserializedObject = JsonSerializer.Deserialize<T>(jsonString);
        return true;
    }
    catch (JsonException) {
        return false;
    }
}

But this does not throw an Exception. Instead it gives me a class object where the property name is NULL.

How can I check if the json string can be deserialized properly into a class?

🌐
DEV Community
dev.to › prameshkc › handling-non-standard-json-in-net-using-systemtextjson-3e97
Handling Non-Standard JSON in .NET Using System.Text.Json - DEV Community
January 4, 2024 - If we try to deserialize the above JSON, we will get the following exception: System.Text.Json.JsonException: The JSON object contains a trailing comma at the end which is not supported in this mode.
Top answer
1 of 1
6

Your JSON root object consists of certain fixed keys ("sol_keys" and "validity_checks") whose values each have some fixed schema, and any number of variable keys (the "782" numeric keys) whose values all share a common schema that differs from the schemas of the fixed key values:

{
  "782": {
    // Properties corresponding to your MarsWheather object
  },
  "783": {
    // Properties corresponding to your MarsWheather object
  },
  // Other variable numeric key/value pairs corresponding to KeyValuePair<string, MarsWheather>
  "sol_keys": [
    // Some array values you don't care about
  ],
  "validity_checks": {
    // Some object you don't care about
  }
}

You would like to deserialize just the variable keys, but when you try to deserialize to a Dictionary<string, MarsWheather> you get an exception because the serializer tries to deserialize a fixed key value as if it were variable key value -- but since the fixed key has an array value while the variable keys have object values, an exception gets thrown. How can System.Text.Json be told to skip the known, fixed keys rather than trying to deserialize them?

If you want to deserialize just the variable keys and skip the fixed, known keys, you will need to create a custom JsonConverter. The easiest way to do that would be to first create some root object for your dictionary:

[JsonConverter(typeof(MarsWheatherRootObjectConverter))]
public class MarsWheatherRootObject
{
    public Dictionary<string, MarsWheather> MarsWheathers { get; } = new Dictionary<string, MarsWheather>();
}

And then define the following converter for it as follows:

public class MarsWheatherRootObjectConverter : FixedAndvariablePropertyNameObjectConverter<MarsWheatherRootObject, Dictionary<string, MarsWheather>, MarsWheather>
{
    static readonly Dictionary<string, ReadFixedKeyMethod> FixedKeyReadMethods = new Dictionary<string, ReadFixedKeyMethod>(StringComparer.OrdinalIgnoreCase)
    {
        { "sol_keys", (ref Utf8JsonReader reader, MarsWheatherRootObject obj, string name, JsonSerializerOptions options) => reader.Skip() },
        { "validity_checks", (ref Utf8JsonReader reader, MarsWheatherRootObject obj, string name, JsonSerializerOptions options) => reader.Skip() },
    };

    protected override Dictionary<string, MarsWheather> GetDictionary(MarsWheatherRootObject obj) => obj.MarsWheathers;
    protected override void SetDictionary(MarsWheatherRootObject obj, Dictionary<string, MarsWheather> dictionary) => throw new RowNotInTableException();
    protected override bool TryGetFixedKeyReadMethod(string name, JsonSerializerOptions options, out ReadFixedKeyMethod method) => FixedKeyReadMethods.TryGetValue(name, out method);
    protected override IEnumerable<KeyValuePair<string, WriteFixedKeyMethod>> GetFixedKeyWriteMethods(JsonSerializerOptions options) => Enumerable.Empty<KeyValuePair<string, WriteFixedKeyMethod>>();
}

public abstract class FixedAndvariablePropertyNameObjectConverter<TObject, TDictionary, TValue> : JsonConverter<TObject> 
    where TDictionary : class, IDictionary<string, TValue>, new()
    where TObject : new()
{
    protected delegate void ReadFixedKeyMethod(ref Utf8JsonReader reader, TObject obj, string name, JsonSerializerOptions options);
    protected delegate void WriteFixedKeyMethod(Utf8JsonWriter writer, TObject value, JsonSerializerOptions options);
        
    protected abstract TDictionary GetDictionary(TObject obj);
    protected abstract void SetDictionary(TObject obj, TDictionary dictionary);
    protected abstract bool TryGetFixedKeyReadMethod(string name, JsonSerializerOptions options, out ReadFixedKeyMethod method);
    protected abstract IEnumerable<KeyValuePair<string, WriteFixedKeyMethod>> GetFixedKeyWriteMethods(JsonSerializerOptions options);
        
    public override TObject Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType == JsonTokenType.Null)
            return (typeToConvert.IsValueType && Nullable.GetUnderlyingType(typeToConvert) == null)
                ? throw new JsonException(string.Format("Unepected token {0}", reader.TokenType))
                : default(TObject);
        if (reader.TokenType != JsonTokenType.StartObject)
            throw new JsonException(string.Format("Unepected token {0}", reader.TokenType));
        var obj = new TObject();
        var dictionary = GetDictionary(obj);
        var valueConverter = (typeof(TValue) == typeof(object) ? null : (JsonConverter<TValue>)options.GetConverter(typeof(TValue))); // Encountered a bug using the builtin ObjectConverter
        while (reader.Read())
        {
            if (reader.TokenType == JsonTokenType.PropertyName)
            {
                var name = reader.GetString();
                reader.ReadAndAssert();
                if (TryGetFixedKeyReadMethod(name, options, out var method))
                {
                    method(ref reader, obj, name, options);
                }
                else
                {
                    if (dictionary == null)
                        SetDictionary(obj, dictionary = new TDictionary());
                    dictionary.Add(name, valueConverter.ReadOrDeserialize(ref reader, typeof(TValue), options));
                }
            }
            else if (reader.TokenType == JsonTokenType.EndObject)
            {
                return obj;
            }
            else
            {
                throw new JsonException(string.Format("Unepected token {0}", reader.TokenType));
            }
        }
        throw new JsonException(); // Truncated file
    }

    public override void Write(Utf8JsonWriter writer, TObject value, JsonSerializerOptions options)
    {
        writer.WriteStartObject();
        var dictionary = GetDictionary(value);
        if (dictionary != null)
        {
            var valueConverter = (typeof(TValue) == typeof(object) ? null : (JsonConverter<TValue>)options.GetConverter(typeof(TValue))); // Encountered a bug using the builtin ObjectConverter
            foreach (var pair in dictionary)
            {
                // TODO: handle DictionaryKeyPolicy 
                writer.WritePropertyName(pair.Key);
                valueConverter.WriteOrSerialize(writer, pair.Value, typeof(TValue), options);
            }
        }
        foreach (var pair in GetFixedKeyWriteMethods(options))
        {
            writer.WritePropertyName(pair.Key);
            pair.Value(writer, value, options);
        }
        writer.WriteEndObject();
    }
}

public static partial class JsonExtensions
{
    public static void WriteOrSerialize<T>(this JsonConverter<T> converter, Utf8JsonWriter writer, T value, Type type, JsonSerializerOptions options)
    {
        if (converter != null)
            converter.Write(writer, value, options);
        else
            JsonSerializer.Serialize(writer, value, type, options);
    }

    public static T ReadOrDeserialize<T>(this JsonConverter<T> converter, ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        => converter != null ? converter.Read(ref reader, typeToConvert, options) : (T)JsonSerializer.Deserialize(ref reader, typeToConvert, options);

    public static void ReadAndAssert(this ref Utf8JsonReader reader)
    {
        if (!reader.Read())
            throw new JsonException();
    }
}

And now you will be able to deserialize to MarsWheatherRootObject as follows:

var root = await System.Text.Json.JsonSerializer.DeserializeAsync<MarsWheatherRootObject>(
    stream, 
    new System.Text.Json.JsonSerializerOptions 
    { 
        PropertyNameCaseInsensitive = true 
    });

Demo fiddle #1 here.

Notes:

  • FixedAndvariablePropertyNameObjectConverter<TObject, TDictionary, TValue> provides a general framework for serializing and deserializing objects with fixed and variable properties. If later you decide to deserialize e.g. "sol_keys", you could modify MarsWheatherRootObject as follows:

    [JsonConverter(typeof(MarsWheatherRootObjectConverter))]
    public class MarsWheatherRootObject
    {
        public Dictionary<string, MarsWheather> MarsWheathers { get; } = new Dictionary<string, MarsWheather>();
        public List<string> SolKeys { get; set; } = new List<string>();
    }
    

    And the converter as follows:

    public class MarsWheatherRootObjectConverter : FixedAndvariablePropertyNameObjectConverter<MarsWheatherRootObject, Dictionary<string, MarsWheather>, MarsWheather>
    {
        static readonly Dictionary<string, ReadFixedKeyMethod> FixedKeyReadMethods = new(StringComparer.OrdinalIgnoreCase)
        {
            { "sol_keys", (ref Utf8JsonReader reader, MarsWheatherRootObject obj, string name, JsonSerializerOptions options) => 
                {
                    obj.SolKeys = JsonSerializer.Deserialize<List<string>>(ref reader, options);
                } 
            },
            { "validity_checks", (ref Utf8JsonReader reader, MarsWheatherRootObject obj, string name, JsonSerializerOptions options) => reader.Skip() },
        };
        static readonly Dictionary<string, WriteFixedKeyMethod> FixedKeyWriteMethods = new Dictionary<string, WriteFixedKeyMethod>()
        {
            { "sol_keys", (w, v, o) => 
                {
                    JsonSerializer.Serialize(w, v.SolKeys, o);
                } 
            },
        };
    
        protected override Dictionary<string, MarsWheather> GetDictionary(MarsWheatherRootObject obj) => obj.MarsWheathers;
        protected override void SetDictionary(MarsWheatherRootObject obj, Dictionary<string, MarsWheather> dictionary) => throw new RowNotInTableException();
        protected override bool TryGetFixedKeyReadMethod(string name, JsonSerializerOptions options, out ReadFixedKeyMethod method) => FixedKeyReadMethods.TryGetValue(name, out method);
        protected override IEnumerable<KeyValuePair<string, WriteFixedKeyMethod>> GetFixedKeyWriteMethods(JsonSerializerOptions options) => FixedKeyWriteMethods;
    }
    

    Demo fiddle #2 here.

Top answer
1 of 16
113

Is polymorphic deserialization possible in System.Text.Json?

The answer is yes and no, depending on what you mean by "possible".

There is no polymorphic deserialization (equivalent to Newtonsoft.Json's TypeNameHandling) support built-in to System.Text.Json prior to .NET 7. This is because reading the .NET type name specified as a string within the JSON payload (such as $type metadata property) to create your objects is not recommended since it introduces potential security concerns (see https://github.com/dotnet/corefx/issues/41347#issuecomment-535779492 for more info).

Allowing the payload to specify its own type information is a common source of vulnerabilities in web applications.

However, there is a way to add your own support for polymorphic deserialization by creating a JsonConverter<T>, so in that sense, it is possible.

The docs show an example of how to do that using a type discriminator property: https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-converters-how-to#support-polymorphic-deserialization

Let's look at an example.

Say you have a base class and a couple of derived classes:

public class BaseClass
{
    public int Int { get; set; }
}
public class DerivedA : BaseClass
{
    public string Str { get; set; }
}
public class DerivedB : BaseClass
{
    public bool Bool { get; set; }
}

You can create the following JsonConverter<BaseClass> that writes the type discriminator while serializing and reads it to figure out which type to deserialize. You can register that converter on the JsonSerializerOptions.

public class BaseClassConverter : JsonConverter<BaseClass>
{
    private enum TypeDiscriminator
    {
        BaseClass = 0,
        DerivedA = 1,
        DerivedB = 2
    }

    public override bool CanConvert(Type type)
    {
        return typeof(BaseClass).IsAssignableFrom(type);
    }
    
    public override BaseClass Read(
        ref Utf8JsonReader reader,
        Type typeToConvert,
        JsonSerializerOptions options)
    {
        if (reader.TokenType != JsonTokenType.StartObject)
        {
            throw new JsonException();
        }

        if (!reader.Read()
                || reader.TokenType != JsonTokenType.PropertyName
                || reader.GetString() != "TypeDiscriminator")
        {
            throw new JsonException();
        }
        
        if (!reader.Read() || reader.TokenType != JsonTokenType.Number)
        {
            throw new JsonException();
        }

        BaseClass baseClass;
        TypeDiscriminator typeDiscriminator = (TypeDiscriminator)reader.GetInt32();
        switch (typeDiscriminator)
        {
            case TypeDiscriminator.DerivedA:
                if (!reader.Read() || reader.GetString() != "TypeValue")
                {
                    throw new JsonException();
                }
                if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
                {
                    throw new JsonException();
                }
                baseClass = (DerivedA)JsonSerializer.Deserialize(ref reader, typeof(DerivedA));
                break;
            case TypeDiscriminator.DerivedB:
                if (!reader.Read() || reader.GetString() != "TypeValue")
                {
                    throw new JsonException();
                }
                if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject)
                {
                    throw new JsonException();
                }
                baseClass = (DerivedB)JsonSerializer.Deserialize(ref reader, typeof(DerivedB));
                break;
            default:
                throw new NotSupportedException();
        }

        if (!reader.Read() || reader.TokenType != JsonTokenType.EndObject)
        {
            throw new JsonException();
        }

        return baseClass;
    }

    public override void Write(
        Utf8JsonWriter writer,
        BaseClass value,
        JsonSerializerOptions options)
    {
        writer.WriteStartObject();

        if (value is DerivedA derivedA)
        {
            writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.DerivedA);
            writer.WritePropertyName("TypeValue");
            JsonSerializer.Serialize(writer, derivedA);
        }
        else if (value is DerivedB derivedB)
        {
            writer.WriteNumber("TypeDiscriminator", (int)TypeDiscriminator.DerivedB);
            writer.WritePropertyName("TypeValue");
            JsonSerializer.Serialize(writer, derivedB);
        }
        else
        {
            throw new NotSupportedException();
        }

        writer.WriteEndObject();
    }
}

This is what serialization and deserialization would look like (including comparison with Newtonsoft.Json):

private static void PolymorphicSupportComparison()
{
    var objects = new List<BaseClass> { new DerivedA(), new DerivedB() };

    // Using: System.Text.Json
    var options = new JsonSerializerOptions
    {
        Converters = { new BaseClassConverter() },
        WriteIndented = true
    };

    string jsonString = JsonSerializer.Serialize(objects, options);
    Console.WriteLine(jsonString);
    /*
     [
      {
        "TypeDiscriminator": 1,
        "TypeValue": {
            "Str": null,
            "Int": 0
        }
      },
      {
        "TypeDiscriminator": 2,
        "TypeValue": {
            "Bool": false,
            "Int": 0
        }
      }
     ]
    */

    var roundTrip = JsonSerializer.Deserialize<List<BaseClass>>(jsonString, options);


    // Using: Newtonsoft.Json
    var settings = new Newtonsoft.Json.JsonSerializerSettings
    {
        TypeNameHandling = Newtonsoft.Json.TypeNameHandling.Objects,
        Formatting = Newtonsoft.Json.Formatting.Indented
    };

    jsonString = Newtonsoft.Json.JsonConvert.SerializeObject(objects, settings);
    Console.WriteLine(jsonString);
    /*
     [
      {
        "$type": "PolymorphicSerialization.DerivedA, PolymorphicSerialization",
        "Str": null,
        "Int": 0
      },
      {
        "$type": "PolymorphicSerialization.DerivedB, PolymorphicSerialization",
        "Bool": false,
        "Int": 0
      }
     ]
    */

    var originalList = JsonConvert.DeserializeObject<List<BaseClass>>(jsonString, settings);

    Debug.Assert(originalList[0].GetType() == roundTrip[0].GetType());
}

Here's another StackOverflow question that shows how to support polymorphic deserialization with interfaces (rather than abstract classes), but a similar solution would apply for any polymorphism: Is there a simple way to manually serialize/deserialize child objects in a custom converter in System.Text.Json?

2 of 16
87

Polymorphic serialization of whitelisted inherited types has been implemented in .NET 7, and is available in Preview 6.

From the documentation page What’s new in System.Text.Json in .NET 7: Type Hierarchies:

System.Text.Json now supports polymorphic serialization and deserialization of user-defined type hierarchies. This can be enabled by decorating the base class of a type hierarchy with the new JsonDerivedTypeAttribute.

First, let's consider serialization. Say you have the following type hierarchy:

public abstract class BaseType { } // Properties omitted

public class DerivedType1 : BaseType { public string Derived1 { get; set; } } 
public class DerivedType2 : BaseType { public int Derived2 { get; set; } }

And you have a data model that includes a value whose declared type is BaseType, e.g.

var list = new List<BaseType> { new DerivedType1 { Derived1 = "value 1" } };

In previous versions, System.Text.Json would only serialize the properties of the declared type BaseType. Now you will be able to include the properties of DerivedType1 when serializing a value declared as BaseType by adding [JsonDerivedType(typeof(TDerivedType))] to BaseType for all derived types:

[JsonDerivedType(typeof(DerivedType1))]
[JsonDerivedType(typeof(DerivedType2))]
public abstract class BaseType { } // Properties omitted

Having whitelisted DerivedType1 in this manner, serialization of your model:

var json = JsonSerializer.Serialize(list);

Results in

[{"Derived1" : "value 1"}]

Demo fiddle #1 here.

Do note that only derived types whitelisted via attribute (or through setting JsonTypeInfo.PolymorphismOptions in runtime) can be serialized via this mechanism. If you have some other derived type which is not whitelisted, e.g.:

public class DerivedType3 : BaseType { public string Derived3 { get; set; } } 

Then JsonSerializer.Serialize(new BaseType [] { new DerivedType3 { Derived3 = "value 3" } }) will throw a System.NotSupportedException: Runtime type 'DerivedType3' is not supported by polymorphic type 'BaseType' exception. Demo fiddle #2 here.

That covers serialization. If you need to round-trip your type hierarchy, you will need to supply a type discriminator property value to use for each derived type. This may be done providing a value for JsonDerivedTypeAttribute.TypeDiscriminator for each derived type:

[JsonDerivedType(typeof(DerivedType1), "DerivedType1")]
[JsonDerivedType(typeof(DerivedType2), "DerivedType2")]
public abstract class BaseType { } // Properties omitted

Now when you serialize your model

var json = JsonSerializer.Serialize(list);

System.Text.Json will add an artificial type discriminator property "$type" indicating the type that was serialized:

[{"$type" : "DerivedType1", "Derived1" : "value 1"}]

Having done so, you can now deserialize your data model like so:

var list2 = JsonSerializer.Deserialize<List<BaseType>>(json);

And the actual, concrete type(s) serialized will be preserved. Demo fiddle #3 here.

It is also possible to inform System.Text.Json of your type hierarchy in runtime via Contract Customization. You might need to do this when your type hierarchy cannot be modified, or when some derived types are in different assemblies and cannot be referenced at compile time, or you are trying to interoperate between multiple legacy serializers. The basic workflow here will be to instantiate an instance of DefaultJsonTypeInfoResolver and add a modifier which sets up the necessary PolymorphismOptions for the JsonTypeInfo for your base type.

For example, polymorphic serialization for the BaseType hierarchy can be enabled in runtime like so:

var resolver = new DefaultJsonTypeInfoResolver
{
    Modifiers =
    {
        // Add an Action<JsonTypeInfo> modifier that sets up the polymorphism options for BaseType
        static typeInfo =>
        {
            if (typeInfo.Type != typeof(BaseType))
                return;
            typeInfo.PolymorphismOptions = new()
            {
                DerivedTypes =
                {
                    new JsonDerivedType(typeof(DerivedType1), "Derived1"),
                    new JsonDerivedType(typeof(DerivedType2), "Derived2")
                }
            };
        },
        // Add other modifiers as required.
    }
};
var options = new JsonSerializerOptions
{
    TypeInfoResolver = resolver,
    // Add other options as required
};
var json = JsonSerializer.Serialize(list, options);

Demo fiddle #4 here.

Notes:

  1. The whitelisting approach is consistent with the approach of the data contract serializers, which use the KnownTypeAttribute, and XmlSerializer, which uses XmlIncludeAttribute. It is inconsistent with Json.NET, whose TypeNameHandling serializes type information for all types unless explicitly filtered via a serialization binder.

    Allowing only whitelisted types to be deserialized prevents Friday the 13th: JSON Attacks type injection attacks including those detailed in TypeNameHandling caution in Newtonsoft Json and External json vulnerable because of Json.Net TypeNameHandling auto?.

  2. Integers as well as strings may be used for the type discriminator name. If you define your type hierarchy as follows:

    [JsonDerivedType(typeof(DerivedType1), 1)]
    [JsonDerivedType(typeof(DerivedType2), 2)]
    public abstract class BaseType { } // Properties omitted
    

    Then serializing the list above results in

    [{"$type" : 1, "Derived1" : "value 1"}]
    

    Numeric type discriminator values are not used by Newtonsoft however, so if you are interoperating with a legacy serializer you might want to avoid this.

  3. The default type discriminator property name, "$type", is the same type discriminator name used by Json.NET. If you would prefer to use a different property name, such as the name "__type" used by DataContractJsonSerializer, apply JsonPolymorphicAttribute to the base type and set TypeDiscriminatorPropertyName like so:

    [JsonPolymorphic(TypeDiscriminatorPropertyName = "__type")]
    [JsonDerivedType(typeof(DerivedType1), "DerivedType1")]
    [JsonDerivedType(typeof(DerivedType2), "DerivedType2")]
    public abstract class BaseType { } // Properties omitted
    
  4. If you are interoperating with Json.NET (or DataContractJsonSerializer), you may set the value of TypeDiscriminator equal to the type discriminator value used by the legacy serializer.

  5. If the serializer encounters a derived type that has not been whitelisted, you can control its behavior by setting JsonPolymorphicAttribute.UnknownDerivedTypeHandling to one of the following values:

    JsonUnknownDerivedTypeHandling Value Meaning
    FailSerialization 0 An object of undeclared runtime type will fail polymorphic serialization.
    FallBackToBaseType 1 An object of undeclared runtime type will fall back to the serialization contract of the base type.
    FallBackToNearestAncestor 2 An object of undeclared runtime type will revert to the serialization contract of the nearest declared ancestor type. Certain interface hierarchies are not supported due to diamond ambiguity constraints.
🌐
GitHub
github.com › dotnet › runtime › issues › 91875
System.Text.Json cannot deserialize to IReadOnlySet · Issue #91875 · dotnet/runtime
September 11, 2023 - JsonSerializer.Deserialize<IReadOnlySet<int>>("[1]"); ought to work, like it does for ISet or other readonly collections e.g. IReadOnlyList. I would also expect IReadOnlySet to be featured in this page https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/supported-collection-types one way or another.
Author   StasJS
🌐
Microsoft Learn
learn.microsoft.com › en-us › answers › questions › 632627 › error-from-system-text-json-trying-to-deserialize
Error from System.Text.Json trying to deserialize a single string - Microsoft Q&A
await JsonSerializer.DeserializeAsync<IEnumerable<T>>(await _client.GetStreamAsync(RelativeUri), new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }) Again, with T being a string. ... A collection is a valid C# type that can be deserialized. Pass a List<string> to the generic method. ... Great! Thanks! ... Your issue is the string is not valid json. A json string has quotes around it. Ex ... So your server is not returning json. The client code should check the content-type. It’s probably text, not json.
🌐
Microsoft Learn
learn.microsoft.com › en-us › dotnet › standard › serialization › system-text-json › overview
Serialize and deserialize JSON using C# - .NET | Microsoft Learn
Access to this page requires authorization. You can try changing directories. ... The System.Text.Json namespace provides functionality for serializing to and deserializing from (or marshalling and unmarshalling) JavaScript Object Notation (JSON). Serialization is the process of converting ...