Using json.net this test fails by default:
JsonConvert.DeserializeObject(
JsonConvert.SerializeObject(new object()),
typeof(object)
).ShouldBeOfType<object>(); // actual type is JObject
Is there a way to change this behavior, so it deserializes to the actual requested type?
You have a degenerate test case there. If you instruct Json.Net to deserialize into type object, you are telling it that the JSON could represent any possible object. So it will choose to use a JObject in that case, since you were not specific and a JObject can handle any JSON object. It is not expecting that you want to deserialize into a literal empty object instance, because that is not a very useful thing to do. If the JSON contained any data at all, you would not be able to access that data after the deserialization: object has no properties!
You can fix your test by creating an empty class Foo and using that in place of object:
JsonConvert.DeserializeObject(
JsonConvert.SerializeObject(new Foo()), typeof(Foo)
).ShouldBeOfType<Foo>();
If you really do need to force Json.Net to deserialize into an empty object instance whenever object is specified as the type, you can do it using a custom JsonConverter like this:
public class EmptyObjectConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(object);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
JToken token = JToken.Load(reader); // consume the JSON object from the reader
return token.Type == JTokenType.Null ? null : new object();
}
public override bool CanWrite
{
get { return false; }
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Then pass an instance of the converter to JsonConvert.DeserializeObject():
JsonConvert.DeserializeObject(
JsonConvert.SerializeObject(new object()),
typeof(object),
new EmptyObjectConverter()
).ShouldBeOfType<object>();
Fiddle: https://dotnetfiddle.net/7xZ7tm
Related
I am trying to deserialize the following JSON (which validates on https://jsonlint.com/):
{"pandoc-api-version":[1,22],"meta":{"title":{"t":"MetaBlocks","c":[{"t":"Para","c":[{"t":"Str","c":"Dynamic"},{"t":"Space"},{"t":"Str","c":"Language"},{"t":"Space"},{"t":"Str","c":"Runtime"}]},{"t":"Para","c":[]}]}},"blocks":[{"t":"Para","c":[{"t":"Strong","c":[{"t":"Str","c":"Bill"},{"t":"Space"},{"t":"Str","c":"Chiles"},{"t":"Space"},{"t":"Str","c":"and"},{"t":"Space"},{"t":"Str","c":"Alex"},{"t":"Space"},{"t":"Str","c":"Turner"}]}]},{"t":"Para","c":[{"t":"Emph","c":[{"t":"Strong","c":[{"t":"Str","c":"Reading"},{"t":"Space"},{"t":"Str","c":"this"},{"t":"Space"},{"t":"Str","c":"Document:"}]}]}]}]}
into the following classes:
internal record TagContent(string T, OneOf<TagContent[], string>? C);
internal class RawPandoc {
[JsonProperty] public int[] PandocApiVersion = default!;
[JsonProperty] public Dictionary<string, TagContent> Meta = default!;
[JsonProperty] public TagContent[] Blocks = default!;
}
using the following code:
var settings = new JsonSerializerSettings {
ContractResolver = new DefaultContractResolver { NamingStrategy = new KebabCaseNamingStrategy() },
Converters = new JsonConverter[] { new OneOfJsonConverter() }
};
var pandoc = JsonConvert.DeserializeObject<RawPandoc>(s, settings);
and I get the following error:
Unexpected token when deserializing object: StartObject. Path 'meta.title.c[0]', line 1, position 69.
How can I resolve this?
For completeness, here is the current and incomplete code for OneOfJsonConverter. OneOf is a library for union types in C#:
using OneOf;
namespace PandocFilters {
public class OneOfJsonConverter : JsonConverter {
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) {
if (value is IOneOf of) {
value = of.Value;
}
serializer.Serialize(writer, value);
}
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) {
if (reader.Value is null) { return null; }
// TODO not implemented yet
return reader.Value;
}
public override bool CanConvert(Type objectType) => objectType.UnderlyingIfNullable().GetInterfaces().Contains(typeof(IOneOf));
}
}
Problem is you are not advancing the reader in your ReadJson implementation. You declared your converter can handle IOneOf objects, and so JSON.NET expects your converter to actually read and handle it, however it does nothing as of now. So ReadJson is called (at the start of first array in json which should be deserialized to OneOf), and then after it returns - reader position is still where it was before (at start of array), which is not what JSON.NET expects. Then it fails trying to continue reading next object, because its assumptions are violated. So, just implement ReadJson, and meanwhile you can advance a reader for example like that:
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) {
// advance reader as expected
var eitherStringOrArray = JObject.ReadFrom(reader);
return reader.Value;
}
https://dotnetfiddle.net/ka6XVw - Fiddle with example type structure
Suppose I have a class that implements IDictionary<string, T>. Json.Net can deserialize such types out of the box, creating an instance of the type and using its indexer to populate the dictionary. The issue is that this class also inherits a string Error property marked with JsonProperty attribute from its base class, and I'd like this property to be populated whenever the input json contains an error field. However, when deserializing an IDictionary Json.Net considers all fields to be dictionary entries and tries to add the value with the error key to the dictionary.
What is the simplest and cleanest way to deserialize the json into a dictionary and the error field into the Error property? Please note that the class is generic, so JsonExtensionData is not an option (without casting its values to the provided type).
Sample valid dictionary json: { 'foo': '1', 'bar': '2' }
Sample error json { 'error': 'blah' }
I've derived a converter solution from this question. Basically, you attach a converter to your DictionaryResponse class, and interpret the incoming JSON yourself. I was lazy enough to use a JObject for parsing:
class DictionaryResponseConverter : JsonConverter<ResponseBase>
{
public override ResponseBase ReadJson(
JsonReader reader, Type objectType,
ResponseBase existingValue, bool hasExistingValue,
JsonSerializer serializer)
{
// find the correct T and call the internal function through reflection
// as DictionaryResponse<T> is sealed, we don't care about inheritance
return (ResponseBase)GetType()
.GetMethod(nameof(InternalReadJson),
BindingFlags.Instance | BindingFlags.NonPublic)
.MakeGenericMethod(objectType.GetGenericArguments()[0])
.Invoke(this, new object[]
{
reader,
existingValue,
hasExistingValue,
serializer
});
}
DictionaryResponse<T> InternalReadJson<T>(
JsonReader reader,
DictionaryResponse<T> existingValue, bool hasExistingValue,
JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
var obj = JObject.Load(reader);
var error = (string)obj["error"];
bool hadError = obj.Remove("error");
//var result = new DictionaryResponse<T>();
var result = hasExistingValue ? existingValue : new DictionaryResponse<T>();
foreach (var kvp in obj)
result[kvp.Key] = kvp.Value.ToObject<T>();
if (hadError)
result.Error = error;
return result;
}
public override void WriteJson(
JsonWriter writer, ResponseBase value, JsonSerializer serializer)
{
// don't care about serialization
throw new NotImplementedException();
}
}
[JsonConverter(typeof(DictionaryResponseConverter))]
internal sealed class DictionaryResponse<T> : ResponseBase, IDictionary<string, T>
{
...
I am writing a Cmdlet and need to pass object structures into an API client that may contain PSObjects. Currently, these serialise as a JSON string containing CLIXML. Instead, I need it to be treated like an object (including the NoteProperties in PSObject.Properties as properties, and recursively serialising their values).
I tried writing my own JsonConverter but for some reason it only gets called for the top level object, not for nested PSObjects:
public class PSObjectJsonConverter : JsonConverter {
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) {
if (value is PSObject) {
JObject obj = new JObject();
foreach (var prop in ((PSObject)value).Properties) {
obj.Add(new JProperty(prop.Name, value));
}
obj.WriteTo(writer);
} else {
JToken token = JToken.FromObject(value);
token.WriteTo(writer);
}
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) {
throw new NotImplementedException();
}
public override bool CanRead {
get { return false; }
}
public override bool CanConvert(Type objectType) {
return true;
}
}
Additionally, I am using serializing to camel case using CamelCasePropertyNamesContractResolver. Is there a way to make the converter respect that?
The following converter should correctly serialize recursively nested objects of type PSObject:
public class PSObjectJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(PSObject).IsAssignableFrom(objectType);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var psObj = (PSObject)value;
writer.WriteStartObject();
foreach (var prop in psObj.Properties)
{
//Probably we shouldn't try to serialize a property that can't be read.
//https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.pspropertyinfo.isgettable?view=powershellsdk-1.1.0#System_Management_Automation_PSPropertyInfo_IsGettable
if (!prop.IsGettable)
continue;
writer.WritePropertyName(prop.Name);
serializer.Serialize(writer, prop.Value);
}
writer.WriteEndObject();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override bool CanRead { get { return false; } }
}
Notes:
In WriteJson you serialize the incoming object value as the value of each property. Surely you meant prop.Value.
By only returning true from CanConvert() when the incoming object type is of type PSObject, you avoid the need to implement default serialization for non-PSObject types in WriteJson().
When you call JToken.FromObject(value) you are not using the incoming JsonSerializer serializer. Thus, any JsonSerializerSettings (including converters) will be lost. In theory you could use JToken.FromObject(Object, JsonSerializer) instead, which would preserve settings, but if you did, you would encounter the bug described in JSON.Net throws StackOverflowException when using [JsonConvert()]. Luckily, since we now return false from CanConvert when default serialization is required, this is no longer necessary.
There is no need to construct an intermediate JObject. You can write directly to the JsonWriter, which will be somewhat more performant.
Update: Additionally, I am using serializing to camel case using CamelCasePropertyNamesContractResolver. Is there a way to make the converter respect that?
Once you introduce a custom JsonConverter for your type, you need to do everything manually, including remapping of property names. Here's a version of WriteJson() that handles this by using DefaultContractResolver.NamingStrategy:
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var psObj = (PSObject)value;
writer.WriteStartObject();
var resolver = serializer.ContractResolver as DefaultContractResolver;
var strategy = (resolver == null ? null : resolver.NamingStrategy) ?? new DefaultNamingStrategy();
foreach (var prop in psObj.Properties)
{
//Probably we shouldn't try to serialize a property that can't be read.
//https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.pspropertyinfo.isgettable?view=powershellsdk-1.1.0#System_Management_Automation_PSPropertyInfo_IsGettable
if (!prop.IsGettable)
continue;
writer.WritePropertyName(strategy.GetPropertyName(prop.Name, false));
serializer.Serialize(writer, prop.Value);
}
writer.WriteEndObject();
}
Note that naming strategies were introduced in Json.NET 9.0.1 so if you are using an earlier version you will need to create your own camel case name mapper such as the one shown in this answer.
I'm using the official Mongo C# Driver, and RestSharp to call a Rest Api with Json.NET to perform my serialization/deserialization. Say I have a Person class as follows, which I'd like to POST & GET:
public class Person
{
[JsonProperty("_id"),JsonConverter(typeof(ObjectIdConverter))]
public ObjectId Id {get;set;}
public string Name {get;set;}
}
I create a new Person object:
var person = new Person{Id = ObjectId.GenerateId(),Name='Joe Bloggs'};
POST it, and on the server I see the following which is correct:
{ _id: 52498b56904ee108c99fbe88, name: 'Joe Bloggs'}
The problem, is when I perform a GET the ObjectId I get on the client is {0000000000000...}
i.e. not the {5249.....} I'd expect. The raw response is showing the correct value, but once I deserialize I loose it.
The ObjectIdConverter code is :
public class ObjectIdConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
serializer.Serialize(writer, value.ToString());
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var objectId = (ObjectId)existingValue; // at this point existingValue is {000...}
return objectId;
}
public override bool CanConvert(Type objectType)
{
return (objectType == typeof (ObjectId));
}
}
Any help would be appreciated.
You are implementing the ReadJson method of the converter incorrectly. The existingValue parameter does not give you the deserialized value read from the JSON, it gives you the existing value of the object that you will be replacing. In most cases this will be null or empty. What you need to do is use the reader to get the value from the JSON, convert it as needed, then return the converted value.
Assuming your ObjectId class has a constructor that accepts a hex string, here is how you would implement the ReadJson method:
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
JToken token = JToken.Load(reader);
return new ObjectId(token.ToObject<string>());
}
Is it possible to override the type resolution using JSON.NET based on a property of the JSON object? Based on existing APIs, it looks like I need a way of accepting a JsonPropertyCollection and returning the Type to be created.
NOTE: I'm aware of the TypeNameHandling attribute, but that adds a $type property. I do not have control over the source JSON.
It would appear that this is handled by creating a custom JsonConverter and adding it to JsonSerializerSettings.Converters before deserialisation.
nonplus has left a handy sample on the JSON.NET discussions board on Codeplex. I've modified the sample to return custom Type and deferring to the default creation mechanism, rather than creating the object instance on the spot.
abstract class JsonCreationConverter<T> : JsonConverter
{
/// <summary>
/// Create an instance of objectType, based properties in the JSON object
/// </summary>
protected abstract Type GetType(Type objectType, JObject jObject);
public override bool CanConvert(Type objectType)
{
return typeof(T).IsAssignableFrom(objectType);
}
public override object ReadJson(JsonReader reader, Type objectType,
object existingValue, JsonSerializer serializer)
{
JObject jObject = JObject.Load(reader);
Type targetType = GetType(objectType, jObject);
// TODO: Change this to the Json.Net-built-in way of creating instances
object target = Activator.CreateInstance(targetType);
serializer.Populate(jObject.CreateReader(), target);
return target;
}
}
And here is the example usage (also updated as mentioned above):
class VehicleConverter : JsonCreationConverter<Vehicle>
{
protected override Type GetType(Type objectType, JObject jObject)
{
var type = (string)jObject.Property("Type");
switch (type)
{
case "Car":
return typeof(Car);
case "Bike":
return typeof(Bike);
}
throw new ApplicationException(String.Format(
"The given vehicle type {0} is not supported!", type));
}
}