1

Is there any way to force newtonsoft to swap a type when it encounters a property of that type during serialization/deserialization?

If I have the following:

public class SomeClass
{
    public SomeNonSerializableType Property { get; set; }
}

If during serialization I encounter a SomeNonSerializableType I want to use Automapper to map it to a SomeSerializableType. I'm assuming I can do this with a contract resolver but not really sure how to implement it. I would then need to do the same in reverse e.g if I encounter a SomeSerializableType during deserialization, map it back to SomeNonSerializableType.

dbc
  • 104,963
  • 20
  • 228
  • 340
ScottKane
  • 47
  • 1
  • 9
  • 1
    If I'm understanding right, you want to to get JSON.Net to use AutoMapper during serialization and deserialization: wouldn't it make more sense to just use AutoMapper to convert the objects before serialization and after deserialization? – stuartd Aug 12 '20 at 15:57
  • Rather why not create a custom converter and let Automapper takeover? https://docs.automapper.org/en/stable/Custom-type-converters.html – Sai Gummaluri Aug 12 '20 at 15:58
  • @stuartd @sai-gummaluri The reason I was thinking to do it through newtonsoft and not automapper is because I want to do it for any time I encounter those types, not specifically mapping `SomeClass` which lead me to thinking a contract resolver with set mapping profiles for the desired types was the easiest way to do it – ScottKane Aug 12 '20 at 16:05

2 Answers2

2

You could create a custom generic JsonConverter that automatically maps from some model to DTO type during serialization, and maps back during deserialization, like so:

public class AutomapperConverter<TModel, TDTO> : JsonConverter
{
    static readonly Lazy<MapperConfiguration> DefaultConfiguration 
        = new Lazy<MapperConfiguration>(() => new MapperConfiguration(cfg => cfg.CreateMap<TModel, TDTO>().ReverseMap()));

    public AutomapperConverter(MapperConfiguration config) => this.config = config ?? throw new ArgumentNullException(nameof(config));
    public AutomapperConverter() : this(DefaultConfiguration.Value) { }
    
    readonly MapperConfiguration config;
    
    public override bool CanConvert(Type type) => type == typeof(TModel);

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var dto = config.CreateMapper().Map<TDTO>(value);
        serializer.Serialize(writer, dto);
    }
    
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var dto = serializer.Deserialize<TDTO>(reader);
        return config.CreateMapper().Map(dto, dto.GetType(), objectType);
    }
}

And then add concrete instances to JsonSerializerSettings.Converters like so:

// Configure this statically on startup
MapperConfiguration configuration 
    = new MapperConfiguration(cfg => 
                              {
                                  // Add all your mapping configurations here.
                                  // ReverseMap() ensures you can map from and to the DTO
                                  cfg.CreateMap<SomeNonSerializableType, SomeSerializableType>().ReverseMap();                                
                              });

// Set up settings using the global configuration.
var settings = new JsonSerializerSettings
{
    Converters = { new AutomapperConverter<SomeNonSerializableType, SomeSerializableType>(configuration) },
};
var json = JsonConvert.SerializeObject(someClass, Formatting.Indented, settings);

var deserialized = JsonConvert.DeserializeObject<SomeClass>(json, settings);

Notes:

Demo fiddle here.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • This is exactly what I was trying to achieve, some generic converter that can handle any model/dto I throw at it via configuration. Really appreciate this in depth answer! – ScottKane Aug 12 '20 at 23:44
0

I just had to do something similar yesterday. Although in my case I only needed to change property name mappings, you can change types with JsonConverter like so:

public enum UserStatus
{
    NotConfirmed,
    Active,
    Deleted
}

public class User
{
    public string UserName { get; set; }

    [JsonConverter(typeof(StringEnumConverter))]
    public UserStatus Status { get; set; }
}

According to the reference:

JsonConverterAttribute The JsonConverterAttribute specifies which JsonConverter is used to convert an object. The attribute can be placed on a class or a member. When placed on a class, the JsonConverter specified by the attribute will be the default way of serializing that class. When the attribute is on a field or property, then the specified JsonConverter will always be used to serialize that value. The priority of which JsonConverter is used is member attribute, then class attribute, and finally any converters passed to the JsonSerializer.

According to the class reference, the type is of System.Type.

It doesn't sound needed, but possibly also of interest may be the JsonConstructorAttribute:

The JsonConstructorAttribute instructs the JsonSerializer to use a specific constructor when deserializing a class. It can be used to create a class using a parameterized constructor instead of the default constructor, or to pick which specific parameterized constructor to use if there are multiple:

public class User
{
    public string UserName { get; private set; }
    public bool Enabled { get; private set; }

    public User()
    {
    }

    [JsonConstructor]
    public User(string userName, bool enabled)
    {
        UserName = userName;
        Enabled = enabled;
    }
}
technonaut
  • 484
  • 3
  • 12
  • Thanks for taking the time to point me in the right direction, really like the generic approach shown by @dbc but this was useful to get me going with the JsonConverter. – ScottKane Aug 12 '20 at 23:47
  • @ScottKane Yeah dbc nailed it, but I will leave this up since it may help someone else later. – technonaut Aug 13 '20 at 13:54