Search code examples
c#.netjson.netautomapper

Newtonsoft JSON auto map specific properties


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.


Solution

  • 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.