@ИгорьОрлов's answer works for when you have types that can only be instantiated directly by JSON.net (due to [JsonConstructor]
and/or use of [JsonProperty]
directly on constructor parameters. However overwriting contract.Converter = null
does not work when JSON.net has already cached the converter to use.
(This wouldn't be an issue if JSON.NET used immutable types to indicate when data and configuration is no-longer mutable, le sigh)
In my case, I did this:
- Implemented a custom
JsonConverter<T>
(where T
is my DTO's base class).
- Defined a
DefaultContractResolver
subclass that overrides ResolveContractConverter
to return my custom JsonConverter
for only the base class.
In detail, and by example:
Supposing I have these immutable DTOs that represent a remote file-system (so there' DirectoryDto
and FileDto
which both inherit FileSystemDto
, just like how DirectoryInfo
and FileInfo
derive from System.IO.FileSystemInfo
):
public enum DtoKind
{
None = 0,
File,
Directory
}
public abstract class FileSystemDto
{
protected FileSystemDto( String name, DtoKind kind )
{
this.Name = name ?? throw new ArgumentNullException(nameof(name));
this.Kind = kind;
}
[JsonProperty( "name" )]
public String Name { get; }
[JsonProperty( "kind" )]
public String Kind { get; }
}
public class FileDto : FileSystemDto
{
[JsonConstructor]
public FileDto(
[JsonProperty("name" )] String name,
[JsonProperty("length")] Int64 length,
[JsonProperty("kind") ] DtoKind kind
)
: base( name: name, kind: kind )
{
if( kind != DtoKind.File ) throw new InvalidOperationException( "blargh" );
this.Length = length;
}
[JsonProperty( "length" )]
public Int64 Length { get; }
}
public class DirectoryDto : FileSystemDto
{
[JsonConstructor]
public FileDto(
[JsonProperty("name")] String name,
[JsonProperty("kind")] DtoKind kind
)
: base( name: name, kind: kind )
{
if( kind != DtoKind.Directory ) throw new InvalidOperationException( "blargh" );
}
}
Supposing I have a JSON array of FileSystemDto
:
[
{ "name": "foo.txt", "kind": "File", "length": 12345 },
{ "name": "bar.txt", "kind": "File", "length": 12345 },
{ "name": "subdir", "kind": "Directory" },
]
I want Json.net to deserialize this to List<FileSystemDto>
...
So define a subclass of DefaultContractResolver
(or if you already have a resolver implementation then subclass (or compose) that) and override ResolveContractConverter
:
public class MyContractResolver : DefaultContractResolver
{
protected override JsonConverter? ResolveContractConverter( Type objectType )
{
if( objectType == typeof(FileSystemDto) )
{
return MyJsonConverter.Instance;
}
else if( objectType == typeof(FileDto ) )
{
// use default
}
else if( objectType == typeof(DirectoryDto) )
{
// use default
}
return base.ResolveContractConverter( objectType );
}
}
Then implement MyJsonConverter
:
public class MyJsonConverter : JsonConverter<FileSystemDto>
{
public static MyJsonConverter Instance { get; } = new MyJsonConverter();
private MyJsonConverter() {}
// TODO: Override `CanWrite => false` and `WriteJson { throw; }` if you like.
public override FileSystemDto? ReadJson( JsonReader reader, Type objectType, FileSystemDto? existingValue, Boolean hasExistingValue, JsonSerializer serializer )
{
if( reader.TokenType == JsonToken.Null ) return null;
if( objectType == typeof(FileSystemDto) )
{
JObject jsonObject = JObject.Load( reader );
if( jsonObject.Property( "kind" )?.Value is JValue jv && jv.Value is String kind )
{
if( kind == "File" )
{
return jsonObject.ToObject<FileDto>( serializer );
}
else if( kind == "Directory" )
{
return jsonObject.ToObject<DirectoryDto>( serializer );
}
}
}
return null; // or throw, depending on your strictness.
}
}
Then, to deserialize, use a JsonSerializer
instance with the ContractResolver
set correctly, for example:
public static IReadOnlyList<FileSystemDto> DeserializeFileSystemJsonArray( String json )
{
JsonSerializer jss = new JsonSerializer()
{
ContractResolver = new KuduDtoContractResolver()
};
using( StringReader strRdr = new StringReader( json ) )
using( JsonTextReader jsonRdr = new JsonTextReader( strRdr ) )
{
List<FileSystemDto>? list = jss.Deserialize< List<FileSystemDto> >( jsonRdr );
// TODO: Throw if `list` is null.
return list;
}
}
there is no $type property
. – Proscribe