Reputation: 33
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 are my interfaces :
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; }
}
Then here is the code I design to tackle my problem :
public class ThumbnailJobModel : IJobModel
{
[JsonProperty( "clientBaseURL" )]
public string ClientBaseURL { get; set; }
[JsonProperty( "userEmail" )]
public string UserEmail { get; set; }
[JsonProperty( "type" )]
[JsonConverter( typeof( TypeConverter ) )]
public ExportType Type { get; set; }
[JsonProperty( "items" )]
[JsonConverter( typeof( ConcreteConverter<List<IItemModel>, List<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" )]
//[JsonConverter( typeof( CamSettingsConverter ) )]
[JsonConverter( typeof( ConcreteConverter<List<ICamSettings>,
List<ShootSettings>> ) )]
public List<ICamSettings> CamSettings { get; set; }
public Item()
{
CamSettings = new List<ICamSettings>();
}
}
public class ShootSettings : ICamSettings
{
[JsonProperty( "orientation" )]
[JsonConverter( typeof( OrientationConverter ) )]
public Orientation Orientation { get; set; }
[JsonProperty( "clothShape" )]
[JsonConverter( typeof( ClothShapeConverter ) )]
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<I, T> : JsonConverter
{
public override bool CanConvert( Type objectType )
{
return typeof( I ) == objectType;
}
public override object ReadJson( JsonReader reader,
Type objectType, object existingValue, JsonSerializer serializer )
{
return serializer.Deserialize<T>( reader );
}
public override void WriteJson( JsonWriter writer,
object value, JsonSerializer serializer )
{
throw new NotImplementedException();
}
}
public class OrientationConverter : JsonConverter
{
public override object ReadJson( JsonReader reader, Type objectType,
object existingValue, JsonSerializer serializer )
{
string enumString = (string)reader.Value;
return Enum.Parse( typeof( Orientation ), enumString, true );
}
public override bool CanConvert( Type objectType )
{
return objectType == typeof( string );
}
public override void WriteJson( JsonWriter writer, object value,
JsonSerializer serializer )
{
throw new NotImplementedException();
}
}
public class ClothShapeConverter : JsonConverter
{
public override object ReadJson( JsonReader reader, Type objectType,
object existingValue, JsonSerializer serializer )
{
var enumString = (string)reader.Value;
return Enum.Parse( typeof( Shape ), enumString, true );
}
public override bool CanConvert( Type objectType )
{
return objectType == typeof( string );
}
public override void WriteJson( JsonWriter writer, object value,
JsonSerializer serializer )
{
throw new NotImplementedException();
}
}
public class TypeConverter : JsonConverter
{
public override object ReadJson( JsonReader reader, Type objectType,
object existingValue, JsonSerializer serializer )
{
return ExportType.Thumbnails;
}
public override bool CanConvert( Type objectType )
{
return objectType == typeof( string );
}
public override void WriteJson( JsonWriter writer, object value,
JsonSerializer serializer )
{
throw new NotImplementedException();
}
}
public static void HandleDeserializationError( object sender,
ErrorEventArgs errorArgs )
{
errorArgs.ErrorContext.Handled = true;
var currentObj = errorArgs.CurrentObject as ShootSettings;
if ( currentObj == null ) return;
currentObj.Orientation = Orientation.Perspective;
currentObj.Shape = Shape.Folded;
}
}
As you can see there is a list of ICamSettings
in the IItemModel
interface.
I try to deserialize this json into my ThumbnailJobModel
class :
{
"clientBaseURL":"https://clientName.fr",
"userEmail":"[email protected]",
"items":[
{
"id":"11913",
"imageSize":"1280,720",
"imagePpi":"72",
"shoots":[
{
"fileName":"front1.png",
"orientation":"front",
"clothShape":"hanger"
},
{
"fileName":"folded1.png",
"orientation":"front",
"clothShape":"folded"
},
{
"fileName":"right1.png",
"orientation":"right",
"clothShape":"hanger"
}
]
},
{
"id":"2988",
"imageSize":"1280,720",
"imagePpi":"",
"shoots":[
{
"fileName":"perspective1.png",
"orientation":"perspective"
}
]
}
]
}
I deserialize my json like that :
//Read the job config
string jobConfig = File.ReadAllText( jsonConfigPath );
IJobModel m_jobModel = JsonConvert.DeserializeObject<ThumbnailJobModel>(
jobConfig );
And the following exception is thrown:
Exception : Error setting value to 'CamSettings' on
'IWD.Screenshoter.Job.ThumbnailJobModel+Item'.
Stack :
at Newtonsoft.Json.Serialization.DynamicValueProvider.SetValue
(System.Object target, System.Object value) [0x00000] in <filename
unknown>:0
at
Newtonsoft.Json.Serialization.JsonSerializerInternalReader.SetPropertyValue
(Newtonsoft.Json.Serialization.JsonProperty property,
Newtonsoft.Json.JsonConverter propertyConverter,
Newtonsoft.Json.Serialization.JsonContainerContract containerContract,
Newtonsoft.Json.Serialization.JsonProperty containerProperty,
Newtonsoft.Json.JsonReader reader, System.Object target) [0x00000] in
<filename unknown>:0
at
Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateObject
(System.Object newObject, Newtonsoft.Json.JsonReader reader,
Newtonsoft.Json.Serialization.JsonObjectContract contract,
Newtonsoft.Json.Serialization.JsonProperty member, System.String id)
[0x00000] in <filename unknown>:0
I honestly don't understand what I am doing wrong, I hope someone will be able to throw some light on it.
Upvotes: 3
Views: 2493
Reputation: 116785
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.
Upvotes: 3