Deserialising mimekit.MimeMessage Object

2.9k views Asked by At

I am having some issues deserialising a mimeKit.mimeMessage that I serialised to a JSON string and stored in a redis key-value cache.

I am able to serialise and store the mimeMessage successfully using either json.NET or Jil, however when I go to deserialise, the following error is thrown.

Thrown by json.NET

Unable to find a constructor to use for type MimeKit.Header. A class should either have a default constructor, one constructor with arguments or a constructor marked with the JsonConstructor attribute. Path 'Headers[0].Offset', line 1, position 22.

I am using the StackExchange.Redis method stringGet to get the serialised object and then passing the RedisValue into Json.Net; code snippet below:

RedisValue result = db.StringGet(key);

MimeMessage message = new MimeMessage();

message = JsonConvert.DeserializeObject<MimeMessage>(result);

It is my understanding that a default constructor is always created, which makes this a little hard to get my head around, could the default constructor have been specified as private, and if so why?

I have also checked the format of the returned JSON string and it is correct.

Thanks for your help.

1

There are 1 answers

2
Andrew Whitaker On BEST ANSWER

First, to clear something up:

It is my understanding that a default constructor is always created, which makes this a little hard to get my head around, could the default constructor have been specified as private, and if so why?

That's not true. When you define a class with no constructors, a default, no-args constructor is generated for you. If you supply a constructor, this default constructor is no longer generated. In other words, a class may not have a default constructor.


With that in mind, the MimeKit.Header class provides 6 constructors, none of which take no arguments. Because of this, JSON.NET does not know how to instantiate this class, which is why the exception occurs.

Since you don't have any control over the MimeKit.Header class, one way to let JSON.NET know how to construct a Header instance is to use a custom converter:

public class MimeHeaderConverter : JsonConverter
{
    public override object ReadJson(
        JsonReader reader,
        Type objectType,
        object existingValue,
        JsonSerializer serializer)
    {
        JObject obj = serializer.Deserialize<JObject>(reader);

        HeaderId headerId = obj["Id"].ToObject<HeaderId>();

        Header header = new Header(headerId, obj["Value"].ToObject<string>());

        return header;
    }

    public override void WriteJson(
        JsonWriter writer,
        object value,
        JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }

    public override bool CanConvert(Type t)
    {
        return typeof(Header).IsAssignableFrom(t);
    }

    public override bool CanRead { get { return true; } }

    public override bool CanWrite { get { return false; } }
}

Here, we're deserializing the Header class into a JObject first. Then, we're picking the HeaderId and Value out of that JObject in order to create a Header instance.

A note here: I don't know much about this library. This may not be the best way to instantiate a Header, and I could be accidentally discarding some information. I only did some very basic tests.

Once you get past this issue, you'll run into another one having to do with property setters that don't accept null values.

Specifically, the ResentMessageId and MimeVersion properties have logic in the setters that throw ArgumentExceptions if you supply null. This is an issue, since those values can be null when the MimeMessage is instantiated.

One workaround for this would be to create a ContractResolver that excludes those properties:

public class MimeMessageContractResolver : DefaultContractResolver
{
    private static HashSet<string> excludedMembers = new HashSet<string>
    {
        "ResentMessageId",
        "MimeVersion"
    };

    protected override List<MemberInfo> GetSerializableMembers(Type objectType)
    {
        var baseMembers = base.GetSerializableMembers(objectType);        

        if (typeof(MimeMessage).IsAssignableFrom(objectType)) 
        {
            baseMembers.RemoveAll(m => excludedMembers.Contains(m.Name));
        }

        return baseMembers;
    }
}

This way, we're never going to accidentally pass null to the setter for one of these properties.

A note here as well: I noticed that even when I ignored the MimeVersion and ResentMessageId they still got set if the headers included a MimeVersion and/or ResentMessageId header. I'm guessing this is baked into the MimeMessage somewhere, but again, I'm not totally familiar with this library.

So to use the classes above (only when deserializing), you'd do this:

string someJsonString = "{ ... }";

MimeMessage deserialized = JsonConvert.DeserializeObject<MimeMessage>(
    someJsonString, new JsonSerializerSettings
    { 
        ContractResolver = new MimeMessageContractResolver(),
        Converters = new JsonConverter[] 
        { 
            new MimeHeaderConverter()
        }
    });

With some basic tests this seemed to work fine. You may end up running into some more issues in actual use, so no guarantees.

Hopefully this will at least get you started.