How do I deserialize NuGet.NuGetVersion into a class in C#? - c#

I'm a beginner, just writing code for an application for my work and I was trying to deserialize a class, but encountered an error.
I have a class named Nuget, that looks like this:
public class Nuget
{
public string? Name { get; set; }
public string? Id { get; set; }
public NuGetVersion Version { get; set; }
public string? PackageSource { get; set; }
public System.DateTimeOffset BlobCreation { get; set; }
}
Now I'm using .Net 6, with System.Text.Json in the method that tries to deserialize a file.
When I try to do it I receive the following error message:
Encountered exception:
System.NotSupportedException: Deserialization of types without a parameterless constructor, a singular parameterized constructor, or a parameterized
constructor annotated with 'JsonConstructorAttribute' is not supported. Type 'NuGet.Versioning.NuGetVersion'. Path: $.Version | LineNumber: 0 |
BytePositionInLine: 65..
Why would NuGetVersion need any constuctor?
The deserializer method is just your usual code.
try
{
Nuget? deserializedObject = JsonSerializer.Deserialize<Nuget>(DirectoryManager.ReadResourceFromExternal(filePath));
if (deserializedObject is null) throw new ArgumentNullException("deserializedObject");
return deserializedObject;
}
catch (Exception ex)
{
LogManager.CatchLog(ex, new string[] {
$"Loading the requested file of {filePath } is not possible\n",
"Please check the filepath"
});
}
The ReadResourceFromExternal method returns the decrypted file in a string format.
public static string ReadResourceFromExternal(string filePath) {
using Stream stream = new MemoryStream(buffer: DAPIManager.Decrypt(filePath));
using StreamReader reader = new(stream);
return reader.ReadToEnd();
}
So far, I'm just trying to understand why NuGet.Version is not acceptable, since it has a {get;set;}, isn't this a parameterless constructor?
I was pondering if I should just have a string instead and convert the NuGet.Version manually.
Or am I misunderstanding the issue here?

Your problem is not with the Nuget.Version property, it is with the NuGetVersion type itself. NuGet.Versioning.NuGetVersion is an immutable type that has multiple parameterized constructors but no parameterless constructor, so System.Text.Json cannot figure out how to construct it when deserializing the value of Nuget.Version.
So, what are your options?
Firstly, Microsoft's own NuGet.Protocol package has a complete set of Newtonsoft.Json converters for serializing and deserializing NuGet types so you could just install that package and use Newtonsoft:
var nuget = Newtonsoft.Json.JsonConvert.DeserializeObject<Nuget>(json,
NuGet.Protocol.JsonExtensions.ObjectSerializationSettings);
Or even more simply:
var nuget = NuGet.Protocol.JsonExtensions.FromJson<Nuget>(json);
Secondly, if you would prefer to stick with System.Text.Json, you will need to port any required converters and settings used in NuGet.Protocol.JsonExtensions from Newtonsoft.
Create the following converters:
public class NuGetVersionConverter : System.Text.Json.Serialization.JsonConverter<NuGetVersion>
{
public override NuGetVersion? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
NuGetVersion.Parse(reader.GetString());
public override void Write(Utf8JsonWriter writer, NuGetVersion value, JsonSerializerOptions options) =>
writer.WriteStringValue(value.ToString());
}
public class VersionRangeConverter : System.Text.Json.Serialization.JsonConverter<VersionRange>
{
public override VersionRange? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
VersionRange.Parse(reader.GetString());
public override void Write(Utf8JsonWriter writer, VersionRange value, JsonSerializerOptions options) =>
// ToString()->Parse()->ToString() adapted from https://github.com/NuGet/NuGet.Client/blob/f24bad0668193ce21a1db8cabd1ce95ba509c7f0/src/NuGet.Core/NuGet.Protocol/Converters/VersionRangeConverter.cs#L41
// Though in all honesty I'm not sure why the round-trip is necessary.
writer.WriteStringValue((VersionRange.Parse(value.ToString())).ToString());
}
// TODO:
// VersionInfoConverter, https://github.com/NuGet/NuGet.Client/blob/3c5cea96aa281d0703d7334d16be14fe8a7e0918/src/NuGet.Core/NuGet.Protocol/Converters/VersionInfoConverter.cs
// FingerprintsConverter, https://github.com/NuGet/NuGet.Client/blob/f24bad0668193ce21a1db8cabd1ce95ba509c7f0/src/NuGet.Core/NuGet.Protocol/Converters/FingerprintsConverter.cs
Then serialize and deserialize using the following options:
var options = new JsonSerializerOptions
{
Converters = { new NuGetVersionConverter(), new VersionRangeConverter(), new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) },
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
// Other options as required
WriteIndented = true,
};
var nuget = JsonSerializer.Deserialize<Nuget>(json, options);
Notes:
The Newtonsoft converters serialize NuGetVersion as a string, so my converter does also:
"Version": "6.0.3-rc.1",
The Json.NET settings used by NuGet.Protocol can be seen here:
public static readonly JsonSerializerSettings ObjectSerializationSettings = new JsonSerializerSettings
{
MaxDepth = JsonSerializationMaxDepth,
NullValueHandling = NullValueHandling.Ignore,
TypeNameHandling = TypeNameHandling.None,
Converters = new List<JsonConverter>
{
new NuGetVersionConverter(),
new VersionInfoConverter(),
new StringEnumConverter { NamingStrategy = new CamelCaseNamingStrategy() },
new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal },
new FingerprintsConverter(),
new VersionRangeConverter()
},
};
The Newtonsoft settings use camel case for enums, so the equivalent System.Text.Json options probably should as well.
Demo fiddle here.

Related

How can I serialize a property in an inherited ICollection<T> class?

I have a class that presented like
public class ItemCollection<T> : ICollection<T> {
public ItemCollection() {
Items = new List<T>();
}
public List<T> Items { get; set; }
...
}
Now it will be serialized into:
{
"Property": [{...}]
}
But I want the result is like:
{
"Property": {"Items": [{...}]}
}
Sorry for the missing information of this question.
I now stuck in serialization when using System.Text.Json.
In Newtonsoft.Json, I use [JsonObject] to annotate this class so it can serialization correctly into json with "Items": value, but I don't know how to serialize the Items property using System.Text.Json.
I have some classes inherited this class and the inheritances will be as properties in other classes.
Solution:
Thank you for every one that answered this question, I have found a solution to solve this. I create a ConverterFactory to resolve the needed types to create the converters. In the converter, I create new JsonObject and use Reflection to create the properties in the type, after this, I serialize the JsonObject so I can get the correct result.
Thank you for your question, but I did not found any difference between the serialization of Newtonsoft and System.Text.Json.
Perhabs you can provide an example in code to show us the problem directly.
With your provided information I got with following code example the same results
static void Main(string[] args)
{
ItemCollection<Person> list = new ItemCollection<Person> {new Person(){ FirstName = "FirstName", Name = "Name"}};
string jsonString = JsonSerializer.Serialize(list);
Console.WriteLine(jsonString);
string jsonString2 =Newtonsoft.Json.JsonConvert.SerializeObject(list);
Console.WriteLine(jsonString2);
Console.ReadLine();
}
[{"Name":"Name","FirstName":"FirstName"}]
There is no built-in attribute corresponding to Newtonsoft's JsonObjectAttribute that will force a collection to be serialized as a JSON object.1. And there is no public equivalent to IContractResolver that can be overridden to customize serialization metadata. Thus you will need to create a custom JsonConverter to serialize the properties of your ItemCollection<T>, such as the following:
[System.Text.Json.Serialization.JsonConverter(typeof(ItemCollectionJsonConverter))]
public partial class ItemCollection<T> : ICollection<T> {
internal ItemCollection(List<T> items) { // I added this for use by the converter
Items = items ?? throw new ArgumentNullException();
}
public ItemCollection() {
Items = new List<T>();
}
public List<T> Items { get; set; }
// Remainder omitted
}
public class ItemCollectionJsonConverter : JsonConverterFactory
{
public override bool CanConvert(Type typeToConvert) => GetItemCollectionValueType(typeToConvert) != null;
public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options)
=> (JsonConverter)Activator.CreateInstance(typeof(ItemCollectionJsonConverterInner<>).MakeGenericType(GetItemCollectionValueType(type)!))!;
static Type? GetItemCollectionValueType(Type type) =>
(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ItemCollection<>)) ? type.GetGenericArguments()[0] : null;
class ItemCollectionJsonConverterInner<T> : JsonConverter<ItemCollection<T>>
{
class ItemCollectionDTO
{
public List<T>? Items { get; set; }
// Add other properties here
}
public override ItemCollection<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
// TODO: Decide whether to throw on null
new ItemCollection<T>((JsonSerializer.Deserialize<ItemCollectionDTO>(ref reader, options)?.Items) ?? throw new JsonException());
public override void Write(Utf8JsonWriter writer, ItemCollection<T> value, JsonSerializerOptions options) =>
JsonSerializer.Serialize(writer, new ItemCollectionDTO { Items = value.Items }, options);
}
}
Notes:
I added the converter directly to ItemCollection<T> via attributes, but you could add it to JsonSerializerOptions.Converters if you prefer.
Many serializers will not serialize collection properties. See e.g. XmlSerializer doesn't serialize everything in my class for another.
Adding properties to collections isn't really a recommended practice; see Why not inherit from List<T>? for a discussion why.
Demo fiddle here.
1 The list of all System.Text.Json serialization attributes is documented here.

Deserialization of JSON to List of Interface with generic type parameter

As the title mentions, I am trying to deserialize a JSON but am having some trouble. I think below includes the necessary information.
public class Variable<T> : IVariable where T : IConvertible
{
//...
}
public class ArrayVariable<T> : IVariable where T : IConvertible
{
//...
}
So I have a list of IVariable which I then serialize successfully (all of the information is in the json):
JsonConvert.SerializeObject(myIVariableList)
Now I am trying to deserialize it but I am having trouble determining the correct way to go about doing it as it involves finding the generic type T in addition to the type Variable or ArrayVariable. I have already tried
JsonConvert.DeserializeObject<List<IVariable>>(result.newValues)
but obviously, you can create instances of an interface. Any help would be much appreciated.
You can use TypeNameHandling.All but I would strongly recommend you avoid it due to it being very dangerous and allows attackers to compromise your code.
Another safer option is to use a custom converter. Here's a very trivial (and fragile) example that should get you started:
First lets make some basic classes that share an interface:
public interface IVariable { }
public class Foo : IVariable
{
public int A { get; set; }
}
public class Bar : IVariable
{
public int B { get; set; }
}
Now we can make our converter:
public class IVariableConverter : JsonConverter<IVariable>
{
public override IVariable ReadJson(JsonReader reader, Type objectType,
IVariable existingValue, bool hasExistingValue, JsonSerializer serializer)
{
// First load the JSON into a JObject
var variable = JObject.Load(reader);
// If the JSON had a property called A, it must be a Foo:
if (variable.ContainsKey("A"))
{
return variable.ToObject<Foo>();
}
// If the JSON had a property called B, it must be a Bar:
if (variable.ContainsKey("B"))
{
return variable.ToObject<Bar>();
}
// And who knows what was passed in if it was missing both of those properties?!
throw new Exception("Er, no idea what that JSON was supposed to be!");
}
public override void WriteJson(JsonWriter writer, IVariable value,
JsonSerializer serializer)
{
// Feel free to write your own code here if you need it
throw new NotImplementedException();
}
}
And now we can do some actual deserialising:
// A basic JSON example:
var json = "[{\"A\":1},{\"B\":2}]";
// The settings to tell the serialiser how to process an IVariable object
var settings = new JsonSerializerSettings
{
Converters = new List<JsonConverter> { new IVariableConverter() }
};
// And deserialise with the defined settings
var result = JsonConvert.DeserializeObject<List<IVariable>>(json, settings);
You will need to be a bit more creative with how you identify each type, but this is a safe way to achieve what you need.
You can use TypeNameHandling.All to add type information to your serialiazed json and then utilize it during parsing:
var variables = new List<IVariable>()
{
new Variable<int>()
};
var settings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All };
var serializeObject = JsonConvert.SerializeObject(variables, settings);
var list = JsonConvert.DeserializeObject<List<IVariable>>(serializeObject, settings);

deserialize json with array of enum

Using the enum:
namespace AppGlobals
{
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum BoardSymbols
{
[EnumMember(Value = "X")]
First = 'X',
[EnumMember(Value = "O")]
Second = 'O',
[EnumMember(Value = "?")]
EMPTY = '?'
}
}
I would like to define a model for my api:
using System;
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using Newtonsoft.Json;
namespace Assignment_1
{
public class MyRequest
{
//...
[Required]
[MinLength(9)]
[MaxLength(9)]
[JsonProperty("changeTypes", ItemConverterType = typeof(JsonStringEnumConverter))]
public AppGlobals.BoardSymbols[] GameBoard { get; set; }
}
}
Where GameBoard should serialize to JSON as an array of strings with names specified by the EnumMember attributes. This approach is adapted from Deserialize json character as enumeration. However, it does not work. This does works if I change the enum to:
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum BoardSymbols
{
X='X',
Y='Y'
}
But I obviously hit a limit on the 'empty' enumeration. How can I do this?
update 2:
I did not have AddNewtonsoftJson() in my startup, converting over fully to Newtonsoft. Now my error is perhaps more actionable:
System.InvalidCastException: Unable to cast object of type 'CustomJsonStringEnumConverter' to type 'Newtonsoft.Json.JsonConverter'.
at Newtonsoft.Json.Serialization.JsonTypeReflector.CreateJsonConverterInstance(Type converterType, Object[] args)
This makes sense, the solution prescribed to me here specified a JsonConverterFactory .. I just need the raw JsonConverter for my use case instead.
TL/DR: You have two basic problems here:
.NET Core 3.0+ has a new built-in JSON serializer System.Text.Json, and you are mixing up attributes and classes between this new serializer and Json.NET. This is very easy to do when both are installed because they share some class names, such as JsonSerializer and JsonConverter.
The new serializer is used by default but does not yet support serialization of enums as strings with custom value names; see System.Text.Json: How do I specify a custom name for an enum value? for details.
The easiest way to solve your problem is to switch back to Json.NET as shown here and use attributes, converters and namespaces exclusively from this serializer.
First let's break down the differences and similarities between the two serializers:
System.Text.Json:
Built into .NET Core 3.0+ automatically and used for JSON serialization by ASP.NET Core 3.0+ by default.
Namespaces System.Text.Json and System.Text.Json.Serialization.
Classes including System.Text.Json.Serialization.JsonConverter, System.Text.Json.Serialization.JsonConverter<T> and System.Text.Json.JsonSerializer.
Attributes including System.Text.Json.Serialization.JsonPropertyNameAttribute, System.Text.Json.Serialization.JsonConverterAttribute and System.Text.Json.Serialization.JsonExtensionDataAttribute.
Serialization of enums as strings is supported by System.Text.Json.Serialization.JsonStringEnumConverter, however renaming via attributes is not implemented.
See this answer to System.Text.Json: How do I specify a custom name for an enum value? for potential workarounds.
Json.NET:
A 3rd-party library that can be used for serialization in ASP.NET Core 3.0+ by adding a NuGet reference to Microsoft.AspNetCore.Mvc.NewtonsoftJson and then calling AddNewtonsoftJson() in Startup.ConfigureServices.
For details see this answer to Where did IMvcBuilder AddJsonOptions go in .Net Core 3.0? by poke.
Namespaces including Newtonsoft.Json, Newtonsoft.Json.Converters, Newtonsoft.Json.Linq and Newtonsoft.Json.Serialization among others.
Classes including Newtonsoft.Json.JsonConverter, Newtonsoft.Json.JsonConverter<T> and Newtonsoft.Json.JsonSerializer
Attributes including Newtonsoft.Json.JsonPropertyAttribute, Newtonsoft.Json.JsonConverterAttribute and Newtonsoft.Json.JsonExtensionDataAttribute among others.
Serialization of enums as renamed strings is supported automatically by Newtonsoft.Json.Converters.StringEnumConverter when the EnumMemberAttribute attribute is applied.
With this in mind, which serializer are you using in your code? Since you helpfully included the namespaces in your question, we can check:
using System.Text.Json.Serialization; // System.Text.Json
using Newtonsoft.Json; // Json.NET
namespace Assignment_1
{
public class MyRequest
{
//...
[JsonProperty( // JsonProperty from Newtonsoft
"changeTypes",
ItemConverterType = typeof(JsonStringEnumConverter)// JsonStringEnumConverter from System.Text.Json
)]
public AppGlobals.BoardSymbols[] GameBoard { get; set; }
}
}
So as you can see, you are mixing up attributes from Newtonsoft with converters from System.Text.Json, which isn't going to work. (Perhaps you selected the namespaces from a "Resolve -> using ..." right-click in Visual Studio?)
So, how to resolve the problem? Since Json.NET supports renaming of enum values out of the box, the easiest way to resolve your problem is to use this serializer. While possibly not as performant as System.Text.Json it is much more complete and full-featured.
To do this, remove the namespaces System.Text.Json.Serialization and System.Text.Json and references to the type JsonStringEnumConverter from your code, and modify MyRequest and BoardSymbols as follows:
using System.Runtime.Serialization;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json;
namespace Assignment_1
{
public class MyRequest
{
//...
[Required]
[MinLength(9)]
[MaxLength(9)]
[JsonProperty("changeTypes")] // No need to add StringEnumConverter here since it's already applied to the enum itself
public AppGlobals.BoardSymbols[] GameBoard { get; set; }
}
}
namespace AppGlobals
{
[JsonConverter(typeof(StringEnumConverter))]
public enum BoardSymbols
{
[EnumMember(Value = "X")]
First = 'X',
[EnumMember(Value = "O")]
Second = 'O',
[EnumMember(Value = "?")]
EMPTY = '?'
}
}
Then NuGet Microsoft.AspNetCore.Mvc.NewtonsoftJson and in Startup.ConfigureServices call AddNewtonsoftJson():
services.AddMvc()
.AddNewtonsoftJson();
Or if you prefer to use StringEnumConverter globally:
services.AddMvc()
.AddNewtonsoftJson(o => o.SerializerSettings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter()));
Do take note of the following comment from the docs
Note: If the AddNewtonsoftJson method isn't available, make sure that you installed the Microsoft.AspNetCore.Mvc.NewtonsoftJson package. A common error is to install the Newtonsoft.Json package instead of the Microsoft.AspNetCore.Mvc.NewtonsoftJson package.
Mockup fiddle here.
You could create you own JsonStringEnumAttribute and decorate your enum with it.
using System.Text.Json.Serialization;
class JsonStringEnumAttribute : JsonConverterAttribute
{
public JsonStringEnumAttribute() : base(typeof(JsonStringEnumConverter))
{
}
}
Then put it on your enum:
[JsonStringEnum]
enum MyEnum
{
Value1,
Value2
}
You can then deserialize JSON like this with the string values:
{
"MyEnumProperty1": "Value1",
"MyEnumProperty2": ["Value2", "Value1"]
}
Into a class like this:
class MyClass
{
MyEnum MyEnumProperty1 { get; set; }
MyEnum[] MyEnumProperty2 { get; set; }
}
Using, for example, System.Net.Http.Json:
using HttpClient client = new();
var myObjects = await client.GetFromJsonAsync<MyClass>("/some-endpoint");
Here is a custom converter to deserialize a list of strings ( ex. from your POST payload) to a list of enums , using JsonConverterFactory.
public class ListOfEnumConverter : JsonConverterFactory
{
public override bool CanConvert(Type typeToConvert)
{
if (!typeToConvert.IsGenericType)
{
return false;
}
if (typeToConvert.GetGenericTypeDefinition() != typeof(List<>))
{
return false;
}
return typeToConvert.GetGenericArguments()[0].IsEnum;
}
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
Type enumType = typeToConvert.GetGenericArguments()[0];
JsonConverter converter = (JsonConverter)Activator.CreateInstance(
typeof(ListOfEnumConverterInner<>).MakeGenericType(
new Type[] { enumType }),
BindingFlags.Instance | BindingFlags.Public,
binder: null,
args: new object[] { options },
culture: null)!;
return converter;
}
}
public class ListOfEnumConverterInner<TEnum> :
JsonConverter<List<TEnum>> where TEnum : struct, Enum
{
private readonly JsonConverter<TEnum> _itemConverter;
private readonly Type _itemType;
public ListOfEnumConverterInner(JsonSerializerOptions options)
{
// For performance, use the existing converter.
_itemConverter = (JsonConverter<TEnum>)options
.GetConverter(typeof(TEnum));
// Cache the enum types.
_itemType = typeof(TEnum);
}
public override List<TEnum>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartArray)
{
throw new JsonException();
}
var enumList = new List<TEnum>();
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndArray)
{
return enumList;
}
// Get the item.
if (reader.TokenType != JsonTokenType.String)
{
throw new JsonException();
}
string? nextItem = reader.GetString();
// For performance, parse with ignoreCase:false first.
if (!Enum.TryParse(nextItem, ignoreCase: false, out TEnum item) &&
!Enum.TryParse(nextItem, ignoreCase: true, out item))
{
throw new JsonException(
$"Unable to convert \"{nextItem}\" to Enum \"{_itemType}\".");
}
//add to list now
enumList.Add(item);
}
throw new JsonException();
}
public override void Write(Utf8JsonWriter writer, List<TEnum> enumList, JsonSerializerOptions options)
{
writer.WriteStartArray();
foreach (TEnum item in enumList)
{
var nextItem = item.ToString();
writer.WriteStringValue
(options.PropertyNamingPolicy?.ConvertName(nextItem) ?? nextItem);
_itemConverter.Write(writer, item, options);
}
writer.WriteEndArray();
}
}
Note: This can also serialize a List of enums, to a list of strings.
Now, all you need to do is to decorate your input model properties with attributes, that point to this converter like this
public class ModelWithEnum
{
public int test1 { get; set; }
[json.JsonConverter(typeof(JsonStringEnumConverter))]
public ServiceType test2 { get; set; }
[json.JsonConverter(typeof(ListOfEnumConverter))]
public List<ServiceType> test3 { get; set; }
}
Hope this helps !
Give me a thumbs-up, if this saves you few hours ;)

Deserialising JSON using JsonSerializer.DeserializeAsync is not using my JsonConverter

A server is returning a JSON string value which is a URL query string:
{
"parameters": "key1=value1&key2=value2"
}
I have a property set up to receive this, and convert it into a Dictionary as part of the deserialisation process:
Property with JsonConverter attribute:
[JsonConverter(typeof(QueryStringToDictionaryJsonConverter))]
public Dictionary<string, string> Parameters { get; set; }
Converter:
public class QueryStringToDictionaryJsonConverter : JsonConverter<Dictionary<string, string>> {
public override Dictionary<string, string> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
var queryString = reader.GetString();
if (string.IsNullOrEmpty(queryString)) return null;
return QueryHelpers.ParseQuery(queryString).ToDictionary(e => e.Key, e => string.Join(",", e.Value.ToArray()));
}
...
}
This should work.
But it's not even getting to my converter.
From what I can tell, JsonSerializer.DeserializeAsync<T>(myJson) is seeing that the type of property is a Dictionary, and so it tries to parse the value as such on its own, and fails (the resulting exception is an 'invalid cast' as it tries to GetEnumerable() etc). A breakpoint in my converter never even gets hit.
I can get it to work by making the property an object and then casting to a Dictionary later where it's used, but that's an ugly solution.
Is there a way to force JsonSerializer.DeserializeAsync<T>(myJson) to just use my converter, without it trying to be smart on its own?
(I'm using Microsoft's System.Text.Json in .NET Core 3)
OK, so this could be a bug in System.Text.Json.
Here's the workaround I'm currently using for anyone else needing a solution.
First, I set up two properties for deserialisation, using [JsonPropertyName] and [JsonIgnore]:
[JsonPropertyName("parameters"), JsonConverter(typeof(QueryStringToDictionaryJsonConverter))]
public object ParametersObject { get; set; }
[JsonIgnore]
public Dictionary<string, string> Parameters => ParametersObject as Dictionary<string, string>;
And then in the JsonConverter, I allow object as the type:
public override bool CanConvert(Type typeToConvert) {
if (typeToConvert == typeof(object)) return true;
return base.CanConvert(typeToConvert);
}
Consumers of my deserialised class just use the Parameters property, which will continue to work just fine if and when this bug is fixed and I change the class back to how I'd like it.
I would create a wrapper and create a converter for the wrapper.
[JsonConverter( typeof( QueryStringDictionaryConverter ) )]
class QueryStringDictionary : Dictionary<string,string> { }
class QueryStringDictionaryConverter : JsonConverter<QueryStringDictionary>
{
...
}
class MyClass
{
public QueryStringDictionary Parameters { get; set; }
}
Alternatively you could use JsonSerializerOptions
class MyOtherClass
{
public Dictionary<string,string> Parameters { get; set; }
}
MyOtherClass Deserialize( string json )
{
var options = new JsonSerializerOptions
{
Converters = { new QueryStringToDictionaryJsonConverter() }
};
return JsonSerializer.Deserialize<MyOtherClass>( json, options );
}
A potential problem with this approach is that the converter would be used on all Dictionary<string,string> properties, which may not be intended. It would work fine for the simple example in the original question.

Why Json.NET cannot deserialize JSON arrays into object property?

Temporary note: This is NOT a duplicate of the above mentioned post
Let's say I have a server-side class structure like this.
public class Test
{
// this can be any kind of "Tag"
public object Data { get; set; }
}
public class Other
{
public string Test { get; set; }
}
Now a string like this is coming from let's say the client.
{"Data": [{$type: "MyProject.Other, MyProject", "Test": "Test"}] }
When I try to deserialize this into a Test instance, I get a result where the Tag property is a JToken instead of some kind of collection, for example ArrayList or List<object>.
I understand that Json.NET cannot deserialize into a strongly typed list, but I'd expect that it respects that it's at least a list.
Here is my current deserialization code.
var settings = new JsonSerializerSettings()
{
TypeNameHandling = TypeNameHandling.Auto,
};
var str = "{\"Data\": [{\"$type\": \"MyProject.Other, MyProject\", \"Test\": \"Test\"}] }";
var test = JsonConvert.Deserialize<Test>(str, settings);
// this first assertion fails
(test.Data is IList).ShouldBeTrue();
(((IList)test.Data)[0] is Other).ShouldBeTrue();
I'm aware of the fact that if I serialize such a structure, then by default I'll get a { $type: ..., $values: [...]} structure in the JSON string instead of a pure array literal, and that will indeed properly deserialize. However, the client is sending a pure array literal, so I should be able to handle that in some way.
I managed to put together a JsonConverter to handle these kind of untyped lists. The converter applies when the target type is object. Then if the current token type is array start ([) it will force a deserialization into List<object>. In any other case it will fall back to normal deserialization.
This is a first version which passes my most important unit tests, however as I'm not a Json.NET expert, it might break some things unexpectedly. Please if anyone sees anything what I didn't, leave a comment.
public class UntypedListJsonConverter : JsonConverter
{
public override bool CanWrite => false;
public override bool CanRead => true;
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotSupportedException();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType != JsonToken.StartArray)
{
return serializer.Deserialize(reader);
}
return serializer.Deserialize<List<object>>(reader);
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(object);
}
}
Usage example:
var settings = new JsonSerializerSettings()
{
TypeNameHandling = TypeNameHandling.Auto,
Converters = new[] { new UntypedListJsonConverter() }
};
var str = "{\"Data\": [{\"$type\": \"MyProject.Other, MyProject\", \"Test\": \"Test\"}] }";
var test = JsonConvert.Deserialize<Test>(str, settings);
// now these assertions pass
(test.Data is IList).ShouldBeTrue();
(((IList)test.Data)[0] is Other).ShouldBeTrue();
Try this:
public class Test
{
public Dictionary<string, List<Other>> Data { get; } = new Dictionary<string, List<Other>>();
}
You need to set up the class you are trying to fill from json data to match as closely to the json structure. From the looks of it, the json looks a dictionary where the keys are strings and the values are arrays of Other objects.

Categories