0

I've almost solved my problem but missing the last piece...

I Have a list of object and I want to be able to add value types (that normaly will be serialized to strings) and get them back as the original type. For example: Guids or custom value types.

This is a sample custom value type:

public struct ExtString
{
    private String Value
    {
        get;
        set;
    }

    public static implicit operator ExtString(String str)
    {
        return !String.IsNullOrWhiteSpace(str) ? new ExtString(str) : null;
    }

    public static implicit operator String(ExtString exStr)
    {
        return exStr.ToString();
    }

    public ExtString(String str)
    {
        this.Value = str;
    }

    public override String ToString()
    {
        return this.Value;
    }
}

This is the Custom converter:

public class CustomConverter : JsonConverter
{
    public override Boolean CanConvert(Type objectType)
    {
        return objectType.IsValueType;
    }

    public override bool CanRead => false;

    public override void WriteJson(JsonWriter writer, Object value, JsonSerializer serializer)
    {
        writer.WriteStartObject();

        writer.WritePropertyName("$type");
        writer.WriteValue(value.GetType().AssemblyQualifiedName);

        writer.WritePropertyName("$value");
        writer.WriteValue(value.ToString());

        writer.WriteEndObject();
    }

And here is the sample code for serializing / deserilizing:

    var jsonSerializerSettings = new JsonSerializerSettings()
    {
        TypeNameHandling = TypeNameHandling.All,
        Converters = new JsonConverter[]
        {
            new CustomConverter()
        }
    };

    var list = new List<Object>();

    list.Add(Guid.NewGuid());
    list.Add((ExtString)"Hello World");

    var ser = JsonConvert.SerializeObject(list, Formatting.Indented, jsonSerializerSettings);

    var deser = JsonConvert.DeserializeObject(ser, jsonSerializerSettings);

This works almost all the way... Both the Guid and ExtString is serialized correct and the Guid is even deserialized with correct value (without special handling!), and the ExtString is created correct (on deserialize) but with the value null (constructor is called but str is null).

What am I missing? Why does it work for Guid?

Thanks.

bang
  • 4,982
  • 1
  • 24
  • 28

1 Answers1

0

All you have to do is to specify the JSON property name for your Value property.

public struct ExtString
{
    [JsonProperty("$value")]
    private String Value
    {
     ...

More in depth

The Guid mapping works because it is considered a primitive type and Json.Net knows how to create a new instance and the parameter to pass to (using a JsonPrimitiveContract).

On the contrary, ExtString is resolved using a JsonObjectContract which calls the parameterless constructor of ExtString (it is there even if you don't declare it, because it's a value type) and then assigns the properties values to the corresponding json properties values. But the ExtString struct property name is Value while the JSON property name is $value. So a new istance of ExtString is created but the Value property remains null because there aren't properties with name Value. This is why, in your code, you have a new istance of ExtString with the Value property setted to null.

In the above solution I match the property name of the ExtString struct with the name of the property in the input JSON. After creating an instance of the ExtString struct, a mapping of the $value property will be done with success.

Another solution could be:

public struct ExtString
{
    private String Value
    {
        get;
        set;
    }

    [JsonConstructor]
    public ExtString([JsonProperty("$value")] String str)
    {
        this.Value = str;
    }

In this case, the constructor with the JsonConstructor attribute will be used instead of the parameterless one. Note that the str parameter must be provided with the appropriate JsonProperty attribute which defines the property name (in the input JSON) whose value will be passed to the constructor. If you omit the JsonProperty attribute, str parameter will be null and so your Value property too.

The difference between the two solutions is on the where the Value property is assigned. In the first solution the property is assigned after creating the object, in the second solution the property is assigned by the constructor.

Think it in this way:

//First solution
var myObject = new ExtString();
myObject.Value = "Hello World";

//Second solution
var myObject = new ExtString("Hello World");

I think the first solution give you much more control setting the value because (modifing the setter method) your logic will be always called, no matter how you create the object or when you assign the value.

Source: a pleasant and immersive analysis of the source code.

I know, my English is very bad :)

Community
  • 1
  • 1
Omar Muscatello
  • 1,256
  • 14
  • 25
  • Your english is fine :) Sounds reasonable and I understand the problem and even found another solution: writing the name of the construct var instead of $value when serializing. Unfortunally I've ran into another problem: I'm also registering TypeConverters for handling conversions to and from string (needed for MVC model binding) and Json.Net is checking that someway and throws an error requesting these type to be serialized to and from strings! Am I thinking totally wrong here? All I want to do is to be able to ser/deserialize List with value type objects (eg. HtmlString).... :( – bang Feb 01 '18 at 21:22
  • Closer :) : Found the JsonObjectAttribute that forces the object to be handled as an complext class when ser/deser and that solves it for my own classes, but how the heck do I decorate eg. Guid or HtmlString with this attribute? Or is there ,maybe another solution for system value types? Date works fine but I guess thats because of the builtin converter / handling.... – bang Feb 01 '18 at 21:54
  • I suggest you to take a look at the `ReadJson` method of `JsonConverter`. See this answer: https://stackoverflow.com/a/8031283/7772490. Using `ReadJson` you have the full controll of the deserialization. Hope it helps. – Omar Muscatello Feb 05 '18 at 09:41
  • I'll see how you're thinking, but as far as I can see ReadJson won't work when serializing to Object because the CanConvert(Type objectType) will give you objectType == Object and all types can convert to Objects so you'll end up with a converter that needs to handle all types! Please tell me I'm wrong because this will solve a lot :) ! – bang Feb 06 '18 at 10:43
  • In `CanConvert` method, you can check the current type of the object, not if the type can convert to another type. Try putting `return objectType.IsValueType || objectType == typeof(object);` in `CanConvert` method and then, in the `ReadJson` method, use `JObject obj = JObject.Load(reader);`. The `ReadJson` method should get called only with objects and not List. So you can access to `$type` and `$value` using `obj["$type"]` and `obj["$value"]`. – Omar Muscatello Feb 07 '18 at 07:34
  • Yes I now. But then I have to handle ALL conversions to Object (not only eg Guid and HtmlString) because when CanConvert is called I have no clue if it’s a type I want to hande or not, and when ReadJson is called it’s to late to cancel and let another converter kick in... right? – bang Feb 08 '18 at 10:05