Json.Net - Error getting value from 'ScopeId' on 'System.Net.IPAddress'

23.7k views Asked by At

I am trying to serialize an IPEndpoint object with Json.Net and I get the following error:

Error getting value from ScopeId on System.Net.IPAddress.

The cause of the error is that I am only using the IPV4 properties of the IPAddress object in the endpoint. When the Json parser tries to parse the IPv6 portion, it accesses the ScopeID property which throws a socket exception

The attempted operation is not supported for the type of object referenced" (A null would have sufficed microsoft!)

I was wondering if there may be a workaround for this other than ripping everything apart and coding the address information as a string? At some point I do want to support IPV6. Is there anything that can be done in Json.NET to ignore the error or simply NOT attempt to serialize the ScopeID if the IPAddress family is set to Internetwork instead of InternetworkIPV6?

2

There are 2 answers

4
Brian Rogers On BEST ANSWER

The IPAddress class is not very friendly to serialization, as you've seen. Not only will it throw a SocketException if you try to access the ScopeID field for an IPv4 address, but it will also throw if you try to access the Address field directly for an IPv6 address.

To get around the exceptions, you will need a custom JsonConverter. A converter allows you to tell Json.Net exactly how you'd like it to serialize and/or deserialize a particular type of object. For an IPAddress, it seems the easiest way to get the data that satisfies everyone is simply to convert it to its string representation and back. We can do that in the converter. Here is how I would write it:

class IPAddressConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return (objectType == typeof(IPAddress));
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        writer.WriteValue(value.ToString());
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        return IPAddress.Parse((string)reader.Value);
    }
}

Pretty straightforward, as these things go. But, this is not the end of the story. If you need to go round-trip with your IPEndPoint, then you will need a converter for it as well. Why? Because IPEndPoint does not contain a default constructor, so Json.Net will not know how to instantiate it. Fortunately, this converter is not difficult to write either:

class IPEndPointConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return (objectType == typeof(IPEndPoint));
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        IPEndPoint ep = (IPEndPoint)value;
        JObject jo = new JObject();
        jo.Add("Address", JToken.FromObject(ep.Address, serializer));
        jo.Add("Port", ep.Port);
        jo.WriteTo(writer);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JObject jo = JObject.Load(reader);
        IPAddress address = jo["Address"].ToObject<IPAddress>(serializer);
        int port = (int)jo["Port"];
        return new IPEndPoint(address, port);
    }
}

So, now that we have the converters, how do we use them? Here is a simple example program that demonstrates. It first creates a couple of endpoints, serializes them to JSON using the custom converters, then immediately deserializes the JSON back into endpoints again using the same converters.

public class Program
{
    static void Main(string[] args)
    {
        var endpoints = new IPEndPoint[]
        {
            new IPEndPoint(IPAddress.Parse("8.8.4.4"), 53),
            new IPEndPoint(IPAddress.Parse("2001:db8::ff00:42:8329"), 81)
        };

        var settings = new JsonSerializerSettings();
        settings.Converters.Add(new IPAddressConverter());
        settings.Converters.Add(new IPEndPointConverter());
        settings.Formatting = Formatting.Indented;

        string json = JsonConvert.SerializeObject(endpoints, settings);
        Console.WriteLine(json);

        var endpoints2 = JsonConvert.DeserializeObject<IPEndPoint[]>(json, settings);

        foreach (IPEndPoint ep in endpoints2)
        {
            Console.WriteLine();
            Console.WriteLine("AddressFamily: " + ep.AddressFamily);
            Console.WriteLine("Address: " + ep.Address);
            Console.WriteLine("Port: " + ep.Port);
        }
    }
}

Here is the output:

[
  {
    "Address": "8.8.4.4",
    "Port": 53
  },
  {
    "Address": "2001:db8::ff00:42:8329",
    "Port": 81
  }
]

AddressFamily: InterNetwork
Address: 8.8.4.4
Port: 53

AddressFamily: InterNetworkV6
Address: 2001:db8::ff00:42:8329
Port: 81

Fiddle: https://dotnetfiddle.net/tK7NKY

0
Stelios Adamantidis On

One thing to note regarding the existing accepted and otherwise correct answer:

return (objectType == typeof(IPAddress)); from .NET Core 3.0 onwards is broken. For more details, see the related issue in Github. That happened after a fix introduced in .NET to make IPAddress truly read-only. Details here. There is a new ReadOnlyIPAddress class that subclasses IPAddress and complicates things.

The simple solution is to replace the check above with

return objectType.IsAssignableTo(typeof(IPAddress));

or if you prefer the .NET framework compatible way

return typeof(IPAddress).IsAssignableFrom(objectType);

I'm adding this as an answer as the change is not obvious and without a unit testing suite covering the case it's easy to slip into production.