-2

I am new to JSON and want to convert the following JSON into a list of objects in .NET. The JSON contains two different objects in an array:

[  
   {  
      "type":"line",
      "a":"-1,5; 3,4",
      "b":"2,2; 5,7",
   },
   {  
      "type":"circle",
      "center":"0; 0",
      "radius":15.0,
   }
]

Here are the corresponding C# classes:

public class Line : Shape
{
    public Point StartPoint { get; set; }
    public Point EndPoint { get; set; }
}

public class Circle : Shape
{
    public Point Center { get; set; }
    public double Radius { get; set; }
}

public abstract class Shape
{
    public string type { get; set; }
}

I am not sure how to map the JSON data structure to my C# classes for Line and Circle. Do I need additional classes in .NET that bridge between my C# classes and the JSON data structure? I am also not sure how to handle the coordinates in this case. Is it possible to do this using only .NET, or do I need a library like JSON.NET?

Update: I think I figured out how to deserialize polymorphic objects. I still have a problem with mapping. How do I tell JSON.NET to convert a string to a System.Windows.Point and back?

Vahid
  • 5,144
  • 13
  • 70
  • 146
  • You can do this with Json.NET. Please see [Deserializing polymorphic json classes without type information using json.net](https://stackoverflow.com/q/19307752/3744182), [Json.Net Serialization of Type with Polymorphic Child Object](https://stackoverflow.com/q/29528648/3744182) or [How to implement custom JsonConverter in JSON.NET to deserialize a List of base class objects?](https://stackoverflow.com/q/8030538/3744182). – dbc Jul 27 '19 at 19:22
  • You will also need to map the properties `"a"` to `StartPoint` and `"b"` to `EndPoint`. To do that, see [How can I change property names when serializing with Json.net?](https://stackoverflow.com/q/8796618/3744182). And to map the string value `"-1,5; 3,4"` to your `Point` type, see [Json.Net: Serialize/Deserialize property as a value, not as an object](https://stackoverflow.com/q/40480489/3744182). – dbc Jul 27 '19 at 19:25
  • Pull all those together and you should be able to do what you need. Agree? – dbc Jul 27 '19 at 19:27
  • @dbc Thank you dbc. I will dig into them. – Vahid Jul 27 '19 at 19:39
  • Also, rather than `type` being a read/write property, I'd make it be an abstract get-only enum-valued property that derived classes must implement. To (de) serialize an enum as a string, use `StringEnumConverter`. See: [parsing an enumeration in JSON.net](https://stackoverflow.com/q/7799769/3744182). – dbc Jul 27 '19 at 19:39
  • @dbc How to handle it when my C# classes are initialized with non-default constructors? – Vahid Jul 27 '19 at 19:58
  • That requirement isn't in your question. Probably those answers can be adapted to that scenario, but your question is getting fairly multifaceted. The suggested format for questions on stack overflow is [one question per post](https://meta.stackexchange.com/q/222735), so do you think you could [edit] your question to just state the exact problem you are now facing? – dbc Jul 27 '19 at 20:36
  • @dbc I think I figured it out. – Vahid Jul 27 '19 at 20:38
  • Glad to hear that! Then do you still need help, or shall we close the question as a duplicate? – dbc Jul 27 '19 at 20:48
  • @dbc Thank you dbc. I still have problem with mapping. How do I tell the JSON.Net to convert string to System.Windows.Point and back. – Vahid Jul 27 '19 at 20:51
  • Does [Json.Net: Serialize/Deserialize property as a value, not as an object](https://stackoverflow.com/q/40480489/3744182) answer your question? Have you tried doing `JsonConvert.SerializeObject(new System.Windows.Point(-1.5, 2.4))` and seeing what happens? – dbc Jul 27 '19 at 21:01
  • @dbc The input json file is from a 3rd party. I looked at your answer. but still cannot figure out how to do it ;( – Vahid Jul 27 '19 at 21:02
  • .. Actually it already sort of works because `Point` already has a built-in `TypeConverter` -- but there's a problem: your floating point values are localized to use `,` as decimal separator and `;` as list separator. This is not a good idea; data interchange files should always be culture-invariant. Is there any chance you can fix the format to be culture-invariant? – dbc Jul 27 '19 at 21:03
  • Well OK that's a question then, let me think about a proper answer. In the meantime can you [edit] your question down to indicate your remaining problem? – dbc Jul 27 '19 at 21:03
  • Did my answer about deserializing `Point` address your remaining issue, or should I delete it? It's unclear between this question and your [other question](https://stackoverflow.com/questions/57236221/exception-occurs-when-trying-to-deserialize-using-json-net) where you stand. – dbc Jul 29 '19 at 22:21
  • @dbc Thank you dbc, for some reason I had missed your answer. I managed to do this and understood JSON. Being a desktop developer I had not any meaningful experience with it before. – Vahid Jul 30 '19 at 06:29

1 Answers1

1

Your remaining problem is that you are trying to deserialize a string containing a pair of numbers to a System.Windows.Point when the numbers have been formatted in a locale using a comma for a decimal separator. You will need to create a custom JsonConverter for this situation:

public class PointConverter : JsonConverter
{
    readonly NumberFormatInfo numberFormatInfo = new NumberFormatInfo
    {
        NumberDecimalSeparator = ",",
        NumberGroupSeparator = ".",
    };

    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(Point) || objectType == typeof(Point?);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        switch (reader.MoveToContentAndAssert().TokenType)
        {
            case JsonToken.Null:
                return null;

            case JsonToken.String:
                {
                    var s = (string)reader.Value;

                    var values = s.Split(';');
                    if (values.Length != 2)
                        throw new JsonSerializationException(string.Format("Invalid Point format {0}", s));
                    try
                    {
                        return new Point(double.Parse(values[0], numberFormatInfo), double.Parse(values[1], numberFormatInfo));
                    }
                    catch (Exception ex)
                    {
                        throw new JsonSerializationException(string.Format("Invalid Point format {0}", s), ex);
                    }
                }

            default:
                throw new JsonSerializationException(string.Format("Unexpected token {0}", reader.TokenType));
        }
    }

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

public static partial class JsonExtensions
{
    public static JsonReader MoveToContentAndAssert(this JsonReader reader)
    {
        if (reader == null)
            throw new ArgumentNullException();
        if (reader.TokenType == JsonToken.None)       // Skip past beginning of stream.
            reader.ReadAndAssert();
        while (reader.TokenType == JsonToken.Comment) // Skip past comments.
            reader.ReadAndAssert();
        return reader;
    }

    public static JsonReader ReadAndAssert(this JsonReader reader)
    {
        if (reader == null)
            throw new ArgumentNullException();
        if (!reader.Read())
            throw new JsonReaderException("Unexpected end of JSON stream.");
        return reader;
    }
}

Then deserialize as follows:

var settings = new JsonSerializerSettings
{
    Converters = { new PointConverter() },
};
var root = JsonConvert.DeserializeObject<TRootObject>(jsonString, settings);

You need to use a JsonConverter because, while Json.NET does support serializing objects as strings using their built-in TypeConverter, there seems to be a bug with the PointConverter used to convert Point to and from a string representation. Specifically, this built-in converter is culture-dependent when converting to a string representation, but culture-ignorant when converting back to a Point.

This can be seen in the reference source for PointConverter.ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value):

public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
    if (value == null)
    {
        throw GetConvertFromException(value);
    }

    String source = value as string;

    if (source != null)
    {
        return Point.Parse(source);
    }

    return base.ConvertFrom(context, culture, value);
}

Notice that culture is not passed into Point.Parse()? Conversely, PointConverter.ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) does make make use of the incoming culture when formatting. That inconsistency is a bug.

And to confirm the bug, if you try to round-trip a Point in, say, the German culture, an exception will be thrown. The following code will throw, demonstrating the problem:

TypeDescriptor.GetConverter(typeof(Point)).ConvertFrom(null, CultureInfo.GetCultureInfo("de-DE"), TypeDescriptor.GetConverter(typeof(Point)).ConvertTo(null, CultureInfo.GetCultureInfo("de-DE"), new Point(-1.5, 3.4), typeof(string)))
dbc
  • 104,963
  • 20
  • 228
  • 340