My current project uses SignalR for its network comms, which uses Newtonsoft.Json as its serialiser. After a bit of refactoring, in which everything seemed to work, I suddenly realised I had a couple of failing tests. That’s right kids, testing actually works. The problem was that the refactor changed a type from a simple string to a complex object. Well, a simple object, with just two properties. Here’s what I have:
public class TopSecretHardware
{
public int ID { get; set; }
public string Name { get; set; }
public IPAddressAndPort IPAddress { get; set; }
}
The IPAddress property was previously a string, but now needed to encapsulate the IP address and the port number. Given that this is used elsewhere in the project, I created a separate class to encapsulate the functionality (parsing, validation, etc):
public class IPAddressAndPort
{
public string Address { get; private set; }
public int Port { get; private set; }
public IPAddressAndPort(string address)
{
string[] IPParts = address.Split(new char[] { ':' });
if (IPParts.Length > 1)
{
Address = IPParts[0];
Port = int.Parse(IPParts[1]);
}
}
public override string ToString()
{
return string.Format("{0}:{1}", Address, Port);
}
}
There’s a little more to the validation, but I’ve removed it for brevity.
This all seemed fine until my tests pointed out that the IP address and port weren’t persisting through my application. A simple Console project confirmed that:
var hardware = new TopSecretHardware()
{
ID = 1,
Name = "box 1",
IPAddress = new IPAddressAndPort("192.168.1.71:123")
};
string json = JsonConvert.SerializeObject(hardware);
Console.WriteLine(json);
TopSecretHardware dehardware = JsonConvert.DeserializeObject<TopSecretHardware>(json);
Console.WriteLine("{0} - {1} - {2}", dehardware.ID, dehardware.Name, dehardware.IPAddress);
The output wasn’t what I expected
{"ID":1,"Name":"box 1","IPAddress":{"Address":"192.168.1.71","Port":123}}
1 - box 1 - :0
The serialization seemed OK, but what was happening when the object was deserialised? A bit of digging leads to some properties you can add to your objects to help with serialization, so I decorate my properties:
[JsonProperty("Address")]
public string Address { get; private set; }
[JsonProperty("Port")]
public int Port { get; private set; }
The output of this is even weirder:
{"ID":1,"Name":"box 1","IPAddress":{"Address":"192.168.1.71","Port":123}}
1 - box 1 - :123
Again the serialisation is fine, but not the reverse. The port number comes across, but no address? Confused I put a breakpoint on the constructor and find that when deserialising, only the address is passed in, not the address and port. Aha. Here we have an object with two properties; how does the serialiser know that these should be combined in “address:port” form to pass into the constructor? It doesn’t, of course. So there are two solutions:
- Add a default constructor. The serialiser will use that to construct the object, then fill in the properties based upon the JsonProperty attribute.
- Don’t use the JsonProperty attributes and create a constructor with a signature that matches the number and type of arguments that are serialised, then decorate it with the JsonConstructor attribute. That way the serialiser knows which constructor to use.
So my class is now:
public class IPAddressAndPort
{
public string Address { get; private set; }
public int Port { get; private set; }
public IPAddressAndPort(string address)
{
string[] IPParts = address.Split(new char[] { ':' });
if (IPParts.Length > 1)
{
Address = IPParts[0];
Port = int.Parse(IPParts[1]);
}
}
[JsonConstructor()]
public IPAddressAndPort(string address, int port)
{
Address = address;
Port = port;
}
public override string ToString()
{
return string.Format("{0}:{1}", Address, Port);
}
}
And everything works fine. The lesson here is that when using external libraries you have to learn about them, even if they often just work out of the box. In all this took an hour or two to dig into and solve, but I’m now much more aware of the problems that can arise and that can only be a good thing.