I am currently facing the situation where I get a json file that I can\'t modify and I want the resulting deserialized class to be generic for design purpose.
First here
Your basic problem is that your ConcreteConverter<I, T>
is designed to deserialize something declared as an interface as a concrete type -- e.g. IItemModel
as Item
-- but you are not using it in that way. You are using it to deserialize a concrete list of interfaces as a concrete list of concrete types, e.g.:
[JsonProperty( "items" )]
[JsonConverter( typeof( ConcreteConverter<List<IItemModel>, List<Item>>) )]
public List<IItemModel> Items { get; set; }
Instead, you should apply the converter to the items of the Items
and CamSettings
collections using JsonPropertyAttribute.ItemConverterType like so:
public class ThumbnailJobModel : IJobModel
{
[JsonProperty("items", ItemConverterType = typeof(ConcreteConverter<IItemModel, Item>))]
public List<IItemModel> Items { get; set; }
And
public class Item : IItemModel
{
[JsonProperty("shoots", ItemConverterType = typeof(ConcreteConverter<ICamSettings, ShootSettings>))]
public List<ICamSettings> CamSettings { get; set; }
This should fix the exception. However, there are additional suggestions to be made:
In several converters you have no implementation for WriteJson()
. If you want to use default serialization, you can override CanWrite and return false.
Please rename TypeConverter
to ExportTypeConverter
. TypeConverter
is already used for something else.
OrientationConverter
and ClothShapeConverter
are unnecessary, the built-in StringEnumConverter will serialize and deserialize any enum as a string.
If you want an exception to be thrown for numeric enum values, you could subclass it as StrictStringEnumConverter
and set AllowIntegerValues = false:
public class StrictStringEnumConverter : StringEnumConverter
{
public StrictStringEnumConverter() { this.AllowIntegerValues = false; }
}
You could also make ExportTypeConverter
inherit from StringEnumConverter
so as to get the desired serialization behavior.
In ConcreteConverter
since T
is supposed to be a concrete implementation of I
, you can add a where
constraint to make sure users of the type don't accidentally invert the generic arguments:
public class ConcreteConverter<IInterface, TConcrete> : JsonConverter where TConcrete : IInterface
{
}
I also renamed the generic arguments to something more meaningful.
In several converters you override CanConvert(Type)
and test for the incoming type being a string
, where string
was the type serialized to the file:
public override bool CanConvert( Type objectType )
{
return objectType == typeof( string );
}
When applied directly by attributes, CanConvert()
is never called. When applied by settings, during serialization objectType
is the actual type of the object that is about to get serialized. And when applied by settings, during deserialization objectType
is the declared type of the member whose value is about to get deserialized. It's never the type in the file. Thus in ExportTypeConverter
it should be written as follows:
public override bool CanConvert(Type objectType)
{
return objectType == typeof(ExportType);
}
Or, since the converter is only ever applied by attributes, you could just throw a NotImplementedException
.
I don't see any reason to nest models like Item
inside ThumbnailJobModel
. To me it simply causes additional complexity. You could just make them non-public instead. But this is just a matter of opinion.
Putting all that together you code should look something like:
public interface IJobModel
{
string ClientBaseURL { get; set; }
string UserEmail { get; set; }
ExportType Type { get; set; }
List<IItemModel> Items { get; set; }
}
public interface IItemModel
{
string Id { get; set; }
string ImageSize { get; set; }
string ImagePpi { get; set; }
List<ICamSettings> CamSettings { get; set; }
}
public interface ICamSettings
{
string FileName { get; set; }
}
public enum ExportType
{
Thumbnails,
}
public class ThumbnailJobModel : IJobModel
{
[JsonProperty("clientBaseURL")]
public string ClientBaseURL { get; set; }
[JsonProperty("userEmail")]
public string UserEmail { get; set; }
[JsonProperty("type")]
[JsonConverter(typeof(ExportTypeConverter))]
public ExportType Type { get; set; }
[JsonProperty("items", ItemConverterType = typeof(ConcreteConverter<IItemModel, Item>))]
public List<IItemModel> Items { get; set; }
public ThumbnailJobModel()
{
Type = ExportType.Thumbnails;
Items = new List<IItemModel>();
}
public class Item : IItemModel
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("imageSize")]
public string ImageSize { get; set; }
[JsonProperty("imagePpi")]
public string ImagePpi { get; set; }
[JsonProperty("shoots", ItemConverterType = typeof(ConcreteConverter<ICamSettings, ShootSettings>))]
public List<ICamSettings> CamSettings { get; set; }
public Item()
{
CamSettings = new List<ICamSettings>();
}
}
public class ShootSettings : ICamSettings
{
[JsonProperty("orientation")]
[JsonConverter(typeof(StrictStringEnumConverter))]
public Orientation Orientation { get; set; }
[JsonProperty("clothShape")]
[JsonConverter(typeof(StrictStringEnumConverter))]
public Shape Shape { get; set; }
[JsonProperty("fileName")]
public string FileName { get; set; }
public ShootSettings()
{
Orientation = Orientation.Perspective;
Shape = Shape.Folded;
FileName = null;
}
}
public enum Orientation
{
Perspective = 0,
Oblique = 1,
Front = 2,
Back = 3,
Left = 4,
Right = 5,
Up = 6,
Down = 7
}
public enum Shape
{
Folded = 0,
Hanger = 1,
Mannequin = 2
}
public class ConcreteConverter<IInterface, TConcrete> : JsonConverter where TConcrete : IInterface
{
public override bool CanConvert(Type objectType)
{
return typeof(IInterface) == objectType;
}
public override object ReadJson(JsonReader reader,
Type objectType, object existingValue, JsonSerializer serializer)
{
return serializer.Deserialize<TConcrete>(reader);
}
public override bool CanWrite { get { return false; } }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
public class ExportTypeConverter : StringEnumConverter
{
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
reader.Skip(); // Skip anything at the current reader's position.
return ExportType.Thumbnails;
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(ExportType);
}
}
public class StrictStringEnumConverter : StringEnumConverter
{
public StrictStringEnumConverter() { this.AllowIntegerValues = false; }
}
public static void HandleDeserializationError(object sender, Newtonsoft.Json.Serialization.ErrorEventArgs errorArgs)
{
errorArgs.ErrorContext.Handled = true;
var currentObj = errorArgs.CurrentObject as ShootSettings;
if (currentObj == null) return;
currentObj.Orientation = Orientation.Perspective;
currentObj.Shape = Shape.Folded;
}
}
Sample working .Net fiddle.