I have a json payload that I want to deserialize in a non-trivial way.
{
"destinationId": 123
}
The target class is
public class SomeObject
{
public Destination Destination { get; set; }
}
public class Destination
{
public Destination(int destinationId)
{
Id = destinationId;
}
public int Id { get; set; }
}
To be able to do so, I've created a JsonConverter
that takes care of it.
Here is the ReadJson method:
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (CanConvert(objectType))
{
var value = reader.Value;
if (value is long v)
{
// TODO: this might overflow
return new Destination((int)v);
}
}
return null;
}
I have then decorated the Destination class with a [JsonConverter]
attribute accepting a typeof(DestinationConverter)
.
This works correctly when I use JsonConvert.DeserializeObject<SomeObject>(myString)
(see unit test below) but I'm having issues creating a successful unit test for the JsonConverter
specifically (see second test below).
[Test, AutoData]
public void SomeObject_is_correctly_deserialized(SomeObject testObject)
{
var json = $@"{{""destinationId"":{testObject.Destination.Id}}}";
Console.WriteLine($"json: {json}");
var obj = JsonConvert.DeserializeObject<SomeObject>(json);
Assert.That(obj.Destination.Id, Is.EqualTo(testObject.Destination.Id));
}
[Test, AutoData]
public void ReadJson_can_deserialize_an_integer_as_Destination(DestinationConverter sut, int testValue)
{
JsonReader reader = new JTokenReader(JToken.Parse($"{testValue}"));
var obj = sut.ReadJson(reader, typeof(Destination), null, JsonSerializer.CreateDefault());
var result = obj as Destination;
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.InstanceOf<Destination>());
Assert.That(result.Id, Is.EqualTo(testValue));
}
I've been googling for a way to properly unit-test a converted but I only find examples of people using the whole DeserializeObject
instead of just testing the converter.
PS: I pasted all the necessary code in a .NET Fiddle: https://dotnetfiddle.net/oUXi6k
Thus, to properly unit-test your converter, you need to advance the reader to the first token of the c# object you are trying to read, e.g. like so: JsonReader reader = new JsonTextReader(new StringReader(json)); while (reader. TokenType == JsonToken.
A converter is a class that converts an object or a value to and from JSON. The System. Text. Json namespace has built-in converters for most primitive types that map to JavaScript primitives.
Your basic problem is that, when you create a JsonReader
, it is initially positioned before the first token. This is alluded to in the documentation for JsonToken
:
JsonToken Enumeration
Specifies the type of JSON token.
Members
None
:0
This is returned by the JsonReader if a read method has not been called.
Thus, to properly unit-test your converter, you need to advance the reader to the first token of the c# object you are trying to read, e.g. like so:
JsonReader reader = new JsonTextReader(new StringReader(json));
while (reader.TokenType == JsonToken.None)
if (!reader.Read())
break;
var obj = sut.ReadJson(reader, typeof(Destination), null, JsonSerializer.CreateDefault());
Sample fiddle here.
Having done that, I would suggest you rewrite your converter as follows:
public class DestinationConverter : JsonConverter
{
public override bool CanConvert(System.Type objectType)
{
return objectType == typeof(Destination);
}
public override object ReadJson(Newtonsoft.Json.JsonReader reader, System.Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer)
{
var id = serializer.Deserialize<int?>(reader);
if (id == null)
return null;
return new Destination(id.Value);
}
public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer)
{
// WriteJson() is never called with a null value, instead Json.NET writes null automatically.
writer.WriteValue(((Destination)value).Id);
}
}
By calling serializer.Deserialize<int?>(reader)
inside ReadJson()
, you guarantee that:
null
values are handled during reading.
An exception will get thrown in case of not-well-formed JSON (e.g. a truncated file).
An exception will get thrown in case of invalid JSON (e.g. an object where an integer was expected, or an integer overflow).
The reader will be correctly positioned at the end of the token being read. (In cases where the token is a primitive the reader does not need to be advanced, but for more complex tokens, it does.)
Sample fiddle #2 here.
You might also want to enhance your unit tests to check that:
The reader is correctly positioned after ReadJson()
, e.g. by asserting the the TokenType
and Depth
are correct, or even counting the number of tokens remaining in the JSON stream and asserting it is as expected.
A common mistake when writing a converter is to leave the reader mis-positioned after conversion. When this is done, the object itself is read successfully but all subsequent objects become corrupt. Unit-testing ReadJson()
directly will not catch this unless you assert that the reader is correctly positioned afterwards.
An exception is thrown for a not-well-formed JSON stream, e.g. one that is truncated.
An exception is thrown for an unexpected JSON token, e.g. when an array is encountered where a primitive is expected.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With