Pass additional data to JsonConverter
up vote
1
down vote
favorite
I deserialize object where one of the properties is a foreign key (eg an identity value from a database table). During deserialization I would like to use a JsonConverter to retrieve the corresponding object from a collection.
I know how to write a use custom JsonConverters. I don't know how to pass the collection to the JsonConverter, because the converter is specified at design time (like below), but the collection obviously only exists at runtime:
<JsonConverter(GetType(JSonCustomConverter))>
Public Property SomeProperty As SomePropertyClass
So the JSonCustomConverter's ReadJson should look this:
Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object
If reader.Value Is Nothing Then Return False
Dim value As String = reader.Value.ToString().Trim()
retun MagicallyGetMyCollectionValue(value)
End Function
So the silly function name MagicallyGetMyCollectionValue is just a placeholder to show you where I am stuck. I don't want to access the collection through a global variable, but I don't know how to pass the collection to the ReadJson either.
I would be happy, if someone could point me in the right direction.
Edit: Let me try to give a better example.
Suppose I have the following class:
class ParentObject
<JssonConverter(GetType(JsonCustomConverter))>
Property SomeProperty As SomePropertyClass
end class
I would deserialize my json data like this:
dim result = JsonConvert.DeserializeObject(jsonData, GetType(ParentObject))
Now assume, that the json data doesn't contain the complete representation of an instance of the SomePropertyClass, but only a key value e.g. an key as string. Suppose I have a collection like this:
dim cache as Dictionary(of string, SomePropertyClass)
That cache shall contain all the instances that I need. So my JSonCustomConverter should have a ReadJson Function like this:
Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object
If reader.Value Is Nothing Then Return nothing
Dim value As String = reader.Value.ToString().Trim()
Dim cachedObject as SomePropertyClass = nothing
if cache.TryGetValue(value, cachedObject) then return cachedObject
retun Nothing ' or new SomePropertyClass(value)
End Function
So I want the ReadJson to lookup the instance based on the key value.
How would I pass the cache-Dictionary into the ReadJson function? I could use a singelton class that contains the cache an som getInstance-method to retrieve it, but I wouldn't want to do this.
vb.net json.net
add a comment |
up vote
1
down vote
favorite
I deserialize object where one of the properties is a foreign key (eg an identity value from a database table). During deserialization I would like to use a JsonConverter to retrieve the corresponding object from a collection.
I know how to write a use custom JsonConverters. I don't know how to pass the collection to the JsonConverter, because the converter is specified at design time (like below), but the collection obviously only exists at runtime:
<JsonConverter(GetType(JSonCustomConverter))>
Public Property SomeProperty As SomePropertyClass
So the JSonCustomConverter's ReadJson should look this:
Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object
If reader.Value Is Nothing Then Return False
Dim value As String = reader.Value.ToString().Trim()
retun MagicallyGetMyCollectionValue(value)
End Function
So the silly function name MagicallyGetMyCollectionValue is just a placeholder to show you where I am stuck. I don't want to access the collection through a global variable, but I don't know how to pass the collection to the ReadJson either.
I would be happy, if someone could point me in the right direction.
Edit: Let me try to give a better example.
Suppose I have the following class:
class ParentObject
<JssonConverter(GetType(JsonCustomConverter))>
Property SomeProperty As SomePropertyClass
end class
I would deserialize my json data like this:
dim result = JsonConvert.DeserializeObject(jsonData, GetType(ParentObject))
Now assume, that the json data doesn't contain the complete representation of an instance of the SomePropertyClass, but only a key value e.g. an key as string. Suppose I have a collection like this:
dim cache as Dictionary(of string, SomePropertyClass)
That cache shall contain all the instances that I need. So my JSonCustomConverter should have a ReadJson Function like this:
Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object
If reader.Value Is Nothing Then Return nothing
Dim value As String = reader.Value.ToString().Trim()
Dim cachedObject as SomePropertyClass = nothing
if cache.TryGetValue(value, cachedObject) then return cachedObject
retun Nothing ' or new SomePropertyClass(value)
End Function
So I want the ReadJson to lookup the instance based on the key value.
How would I pass the cache-Dictionary into the ReadJson function? I could use a singelton class that contains the cache an som getInstance-method to retrieve it, but I wouldn't want to do this.
vb.net json.net
@Amessihel: No, I don't think so, because the solution shows how deserialize to one class or another based on the json array. In my case there I know the class to deserialize my property to. I try to add a better example to the question, to make it clearer, what I want (that will take a few minutes).
– Sascha
Nov 7 at 16:36
1
Assuming you are constructing the serializer yourself, you can pass in additional data usingStreamingContext.Context
viaJsonSerializer.Context
. In JSON.NET how to get a reference to every deserialized object? shows an example.
– dbc
Nov 7 at 16:37
@dbc That looks promising. I will try it and leave a comment on how it worked.
– Sascha
Nov 7 at 17:14
if it does work, could ya do me a favor and post the solution as an answer?
– doom87er
Nov 7 at 17:21
@doom87er -- working on one now.
– dbc
Nov 7 at 19:10
add a comment |
up vote
1
down vote
favorite
up vote
1
down vote
favorite
I deserialize object where one of the properties is a foreign key (eg an identity value from a database table). During deserialization I would like to use a JsonConverter to retrieve the corresponding object from a collection.
I know how to write a use custom JsonConverters. I don't know how to pass the collection to the JsonConverter, because the converter is specified at design time (like below), but the collection obviously only exists at runtime:
<JsonConverter(GetType(JSonCustomConverter))>
Public Property SomeProperty As SomePropertyClass
So the JSonCustomConverter's ReadJson should look this:
Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object
If reader.Value Is Nothing Then Return False
Dim value As String = reader.Value.ToString().Trim()
retun MagicallyGetMyCollectionValue(value)
End Function
So the silly function name MagicallyGetMyCollectionValue is just a placeholder to show you where I am stuck. I don't want to access the collection through a global variable, but I don't know how to pass the collection to the ReadJson either.
I would be happy, if someone could point me in the right direction.
Edit: Let me try to give a better example.
Suppose I have the following class:
class ParentObject
<JssonConverter(GetType(JsonCustomConverter))>
Property SomeProperty As SomePropertyClass
end class
I would deserialize my json data like this:
dim result = JsonConvert.DeserializeObject(jsonData, GetType(ParentObject))
Now assume, that the json data doesn't contain the complete representation of an instance of the SomePropertyClass, but only a key value e.g. an key as string. Suppose I have a collection like this:
dim cache as Dictionary(of string, SomePropertyClass)
That cache shall contain all the instances that I need. So my JSonCustomConverter should have a ReadJson Function like this:
Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object
If reader.Value Is Nothing Then Return nothing
Dim value As String = reader.Value.ToString().Trim()
Dim cachedObject as SomePropertyClass = nothing
if cache.TryGetValue(value, cachedObject) then return cachedObject
retun Nothing ' or new SomePropertyClass(value)
End Function
So I want the ReadJson to lookup the instance based on the key value.
How would I pass the cache-Dictionary into the ReadJson function? I could use a singelton class that contains the cache an som getInstance-method to retrieve it, but I wouldn't want to do this.
vb.net json.net
I deserialize object where one of the properties is a foreign key (eg an identity value from a database table). During deserialization I would like to use a JsonConverter to retrieve the corresponding object from a collection.
I know how to write a use custom JsonConverters. I don't know how to pass the collection to the JsonConverter, because the converter is specified at design time (like below), but the collection obviously only exists at runtime:
<JsonConverter(GetType(JSonCustomConverter))>
Public Property SomeProperty As SomePropertyClass
So the JSonCustomConverter's ReadJson should look this:
Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object
If reader.Value Is Nothing Then Return False
Dim value As String = reader.Value.ToString().Trim()
retun MagicallyGetMyCollectionValue(value)
End Function
So the silly function name MagicallyGetMyCollectionValue is just a placeholder to show you where I am stuck. I don't want to access the collection through a global variable, but I don't know how to pass the collection to the ReadJson either.
I would be happy, if someone could point me in the right direction.
Edit: Let me try to give a better example.
Suppose I have the following class:
class ParentObject
<JssonConverter(GetType(JsonCustomConverter))>
Property SomeProperty As SomePropertyClass
end class
I would deserialize my json data like this:
dim result = JsonConvert.DeserializeObject(jsonData, GetType(ParentObject))
Now assume, that the json data doesn't contain the complete representation of an instance of the SomePropertyClass, but only a key value e.g. an key as string. Suppose I have a collection like this:
dim cache as Dictionary(of string, SomePropertyClass)
That cache shall contain all the instances that I need. So my JSonCustomConverter should have a ReadJson Function like this:
Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object
If reader.Value Is Nothing Then Return nothing
Dim value As String = reader.Value.ToString().Trim()
Dim cachedObject as SomePropertyClass = nothing
if cache.TryGetValue(value, cachedObject) then return cachedObject
retun Nothing ' or new SomePropertyClass(value)
End Function
So I want the ReadJson to lookup the instance based on the key value.
How would I pass the cache-Dictionary into the ReadJson function? I could use a singelton class that contains the cache an som getInstance-method to retrieve it, but I wouldn't want to do this.
vb.net json.net
vb.net json.net
edited Nov 7 at 20:10
dbc
52.3k765116
52.3k765116
asked Nov 7 at 16:17
Sascha
91111127
91111127
@Amessihel: No, I don't think so, because the solution shows how deserialize to one class or another based on the json array. In my case there I know the class to deserialize my property to. I try to add a better example to the question, to make it clearer, what I want (that will take a few minutes).
– Sascha
Nov 7 at 16:36
1
Assuming you are constructing the serializer yourself, you can pass in additional data usingStreamingContext.Context
viaJsonSerializer.Context
. In JSON.NET how to get a reference to every deserialized object? shows an example.
– dbc
Nov 7 at 16:37
@dbc That looks promising. I will try it and leave a comment on how it worked.
– Sascha
Nov 7 at 17:14
if it does work, could ya do me a favor and post the solution as an answer?
– doom87er
Nov 7 at 17:21
@doom87er -- working on one now.
– dbc
Nov 7 at 19:10
add a comment |
@Amessihel: No, I don't think so, because the solution shows how deserialize to one class or another based on the json array. In my case there I know the class to deserialize my property to. I try to add a better example to the question, to make it clearer, what I want (that will take a few minutes).
– Sascha
Nov 7 at 16:36
1
Assuming you are constructing the serializer yourself, you can pass in additional data usingStreamingContext.Context
viaJsonSerializer.Context
. In JSON.NET how to get a reference to every deserialized object? shows an example.
– dbc
Nov 7 at 16:37
@dbc That looks promising. I will try it and leave a comment on how it worked.
– Sascha
Nov 7 at 17:14
if it does work, could ya do me a favor and post the solution as an answer?
– doom87er
Nov 7 at 17:21
@doom87er -- working on one now.
– dbc
Nov 7 at 19:10
@Amessihel: No, I don't think so, because the solution shows how deserialize to one class or another based on the json array. In my case there I know the class to deserialize my property to. I try to add a better example to the question, to make it clearer, what I want (that will take a few minutes).
– Sascha
Nov 7 at 16:36
@Amessihel: No, I don't think so, because the solution shows how deserialize to one class or another based on the json array. In my case there I know the class to deserialize my property to. I try to add a better example to the question, to make it clearer, what I want (that will take a few minutes).
– Sascha
Nov 7 at 16:36
1
1
Assuming you are constructing the serializer yourself, you can pass in additional data using
StreamingContext.Context
via JsonSerializer.Context
. In JSON.NET how to get a reference to every deserialized object? shows an example.– dbc
Nov 7 at 16:37
Assuming you are constructing the serializer yourself, you can pass in additional data using
StreamingContext.Context
via JsonSerializer.Context
. In JSON.NET how to get a reference to every deserialized object? shows an example.– dbc
Nov 7 at 16:37
@dbc That looks promising. I will try it and leave a comment on how it worked.
– Sascha
Nov 7 at 17:14
@dbc That looks promising. I will try it and leave a comment on how it worked.
– Sascha
Nov 7 at 17:14
if it does work, could ya do me a favor and post the solution as an answer?
– doom87er
Nov 7 at 17:21
if it does work, could ya do me a favor and post the solution as an answer?
– doom87er
Nov 7 at 17:21
@doom87er -- working on one now.
– dbc
Nov 7 at 19:10
@doom87er -- working on one now.
– dbc
Nov 7 at 19:10
add a comment |
2 Answers
2
active
oldest
votes
up vote
1
down vote
As requested by @doom87er I will share the code, that worked for me. The solution is based on the comment by @dbc, with some changes. Please treat below code more like a conceptual code: I had to change some of the names and leave out some logik, that isn't required for this proof of concept. So there might be typos in it.
The main solution is to subclass the DefaultContractResolver and add the the cache-dictionary to that class. Something like this:
Public Class CacheContractResolver
Inherits DefaultContractResolver
Public Cache As Dictionary(of string, SomePropertyClass)
Public Sub New(preFilledCache As Dictionary(of string, SomePropertyClass)
Me.Cache = preFilledCache
End Sub
End Class
Then you pass the custom contract resolver using the JsonSerializerSettings like this:
Dim settings = New JsonSerializerSettings
settings.ContractResolver = New SupportControllerContractResolver(prefilledCache)
Dim result = JsonConvert.DeserializeObject(Of ParentObject)(jsonData, settings)
where prefilledCache is an instance of a dictionary containing the SomePropertyClass-objects.
The last step is to retrieve the cache in my JsonConverter's ReadJson function (that I attached to the SomeProperty as shown in the original post's sample code):
Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object
Dim cacheResolver = TryCast(serializer.ContractResolver, CacheContractResolver)
if cacheResolver is nothing return nothing ' add some better null handling here
Dim value As String = reader.Value.ToString().Trim()
Dim cachedObject as SomePropertyClass = nothing
if cacheResolver.Cache.TryGetValue(value, cachedObject) then return cachedObject
retun Nothing ' or new SomePropertyClass(value)
End Function
I tried it and it seems to work.
So in a nut shell:
- Subclass the DefaultContractResolver and include all the additional data, that you need.
- Pass an instance of you custom contract resolver with the additional data in the JsonSerializerSettings.
- In your JsonConverter trycast the passed contract resolver back to your custom contract resolver and there you have your additional data.
I would be happy if you comment on any catches that I might miss, but I think this should be solution that I can live with.
Thanks for you coments and help.
Sascha
Thanks. I was having Stack overflow issues with some really complex converters. Hopefully I can sort something out with this
– doom87er
Nov 7 at 20:54
add a comment |
up vote
1
down vote
You can pass additional data to your custom JsonConverter
using StreamingContext.Context
via JsonSerializer.Context
. Using this mechanism it becomes possible to map class instances to names in a generic manner.
First, define the following interfaces and generic converter:
Public Interface ISerializationContext
Function TryGetNameTable(Of T)(ByRef table as INameTable(Of T)) as Boolean
End Interface
Public Interface INameTable(Of T)
Function TryGetName(value As T, ByRef name as String) As Boolean
Function TryGetValue(name as String, ByRef value as T) As Boolean
End Interface
Public Class NameTable(Of T) : Implements INameTable(Of T)
Public Property Dictionary as Dictionary(Of String, T) = New Dictionary(Of String, T)()
Public Property ReverseDictionary as Dictionary(Of T, String) = New Dictionary(Of T, String)()
Public Function Add(value as T, name as String) as T
Dictionary.Add(name, value)
ReverseDictionary.Add(value, name)
Return value
End Function
Public Function TryGetName(value As T, ByRef name as String) As Boolean Implements INameTable(Of T).TryGetName
Return ReverseDictionary.TryGetValue(value, name)
End Function
Function TryGetValue(name as String, ByRef value as T) As Boolean Implements INameTable(Of T).TryGetValue
Return Dictionary.TryGetValue(name, value)
End Function
End Class
Public Class ObjectToNameConverter(Of T)
Inherits JsonConverter
Public Overrides Function CanConvert(objectType As Type) As Boolean
Return GetType(T) = objectType
End Function
Public Overrides Sub WriteJson(writer As JsonWriter, value As Object, serializer As JsonSerializer)
Dim tValue = CType(value, T)
Dim context as ISerializationContext = CType(serializer.Context.Context, ISerializationContext)
If context Is Nothing
Throw New JsonSerializationException("No ISerializationContext.")
End If
Dim nameTable as INameTable(Of T) = Nothing
If (Not context.TryGetNameTable(Of T)(nameTable))
Throw New JsonSerializationException("No NameTable.")
End If
Dim name as String = Nothing
if (Not nameTable.TryGetName(tValue, name))
Throw New JsonSerializationException("No Name.")
End If
writer.WriteValue(name)
End Sub
Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object
Dim context as ISerializationContext = CType(serializer.Context.Context, ISerializationContext)
If context Is Nothing
Throw New JsonSerializationException("No ISerializationContext.")
End If
Dim nameTable as INameTable(Of T) = Nothing
If (Not context.TryGetNameTable(Of T)(nameTable))
Throw New JsonSerializationException("No NameTable.")
End If
Dim name As String = serializer.Deserialize(Of String)(reader)
If name Is Nothing Then
Return Nothing
End If
dim tValue as T = Nothing
nameTable.TryGetValue(name, tValue)
return tValue
End Function
End Class
Next, define the following concrete implementations:
Public Class RootObject
<JsonConverter(GetType(ObjectToNameConverter(Of SomePropertyClass)))> _
Public Property SomeProperty As SomePropertyClass
End Class
Public Class SomePropertyClass
End Class
Public Class MySerializationContext : Implements ISerializationContext
Public Function Add(value as SomePropertyClass, name as String) as SomePropertyClass
Return SomePropertyNameTable.Add(value, name)
End Function
Property SomePropertyNameTable as NameTable(Of SomePropertyClass) = New NameTable(Of SomePropertyClass)
Public Function TryGetNameTable(Of T)(ByRef table as INameTable(Of T)) as Boolean Implements ISerializationContext.TryGetNameTable
if (GetType(T) Is GetType(SomePropertyClass))
table = SomePropertyNameTable
return True
End If
table = Nothing
return False
End Function
End Class
Now, you can replace instances of SomePropertyClass
with their names during deserialization as follows:
Dim context as MySerializationContext = New MySerializationContext()
Dim someProperty as SomePropertyClass = context.Add(New SomePropertyClass(), "My Name")
Dim root as RootObject = New RootObject With { .SomeProperty = someProperty }
Dim settings = new JsonSerializerSettings With _
{ _
.Context = New System.Runtime.Serialization.StreamingContext(System.Runtime.Serialization.StreamingContextStates.All, context)
}
Dim json as String = JsonConvert.SerializeObject(root, settings)
Console.WriteLine(json) ' Prints {"SomeProperty":"My Name"}
dim root2 as RootObject = JsonConvert.DeserializeObject(Of RootObject)(json, settings)
' Assert that the same instance of SomeProperty was used during deserialization
Assert.IsTrue(root2.SomeProperty Is root.SomeProperty)
Assert.IsTrue(json.Equals("{""SomeProperty"":""My Name""}"))
Notes:
ISerializationContext.TryGetNameTable(Of T)(ByRef table as INameTable(Of T))
is generic so that object-to-name replacement can be supported for multiple types of objects simultaneously without the converters interfering with each other.
The concrete implementation need not be so generic, however. Here
MySerializationContext
only supports name replacement for instances ofSomePropertyClass
. Others could be added as needed.
As stated in Does Json.NET cache types' serialization information?, Newtonsoft recommends caching instances of
DefaultContractResolver
and its subtypes for best performance. Thus it may be preferable to pass additional data viaStreamingContext.Context
rather than via freshly allocated instances of subclasses ofDefaultContractResolver
.
Sample working .Net fiddle #1 here.
As an alternative, while the design above works, in my opinion it would be simpler to remove <JsonConverter(GetType(ObjectToNameConverter(Of SomePropertyClass)))>
from SomeProperty
and instead pass an appropriately initialized ObjectToNameConverter(Of SomePropertyClass)
, containing a local reference to some INameTable(Of SomePropertyClass)
, in JsonSerializerSettings.Converters
.
Define the converter and interfaces like so. Notice that ObjectToNameConverter(Of T)
now has a parameterized constructor and that ISerializationContext
is no longer required:
Public Interface INameTable(Of T)
Function TryGetName(value As T, ByRef name as String) As Boolean
Function TryGetValue(name as String, ByRef value as T) As Boolean
End Interface
Public Class NameTable(Of T) : Implements INameTable(Of T)
Public Property Dictionary as Dictionary(Of String, T) = New Dictionary(Of String, T)()
Public Property ReverseDictionary as Dictionary(Of T, String) = New Dictionary(Of T, String)()
Public Function Add(value as T, name as String) as T
Dictionary.Add(name, value)
ReverseDictionary.Add(value, name)
Return value
End Function
Public Function TryGetName(value As T, ByRef name as String) As Boolean Implements INameTable(Of T).TryGetName
Return ReverseDictionary.TryGetValue(value, name)
End Function
Function TryGetValue(name as String, ByRef value as T) As Boolean Implements INameTable(Of T).TryGetValue
Return Dictionary.TryGetValue(name, value)
End Function
End Class
Public Class ObjectToNameConverter(Of T)
Inherits JsonConverter
Private Property NameTable as INameTable(Of T)
Public Sub New(nameTable as INameTable(Of T))
If nameTable Is Nothing
Throw new ArgumentNullException("nameTable")
End If
Me.NameTable = nameTable
End Sub
Public Overrides Function CanConvert(objectType As Type) As Boolean
Return GetType(T) = objectType
End Function
Public Overrides Sub WriteJson(writer As JsonWriter, value As Object, serializer As JsonSerializer)
Dim tValue = CType(value, T)
Dim name as String = Nothing
if (Not NameTable.TryGetName(tValue, name))
Throw New JsonSerializationException("No Name.")
End If
writer.WriteValue(name)
End Sub
Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object
Dim name As String = serializer.Deserialize(Of String)(reader)
If name Is Nothing Then
Return Nothing
End If
dim tValue as T = Nothing
NameTable.TryGetValue(name, tValue)
return tValue
End Function
End Class
Then serialize as follows:
dim nameTable = new NameTable(Of SomePropertyClass)()
Dim someProperty as SomePropertyClass = nameTable.Add(New SomePropertyClass(), "My Name")
Dim root as RootObject = New RootObject With { .SomeProperty = someProperty }
Dim settings = new JsonSerializerSettings()
settings.Converters.Add(new ObjectToNameConverter(Of SomePropertyClass)(nameTable))
Dim json as String = JsonConvert.SerializeObject(root, settings)
Console.WriteLine(json) ' Prints {"SomeProperty":"My Name"}
dim root2 as RootObject = JsonConvert.DeserializeObject(Of RootObject)(json, settings)
' Assert that the same instance of SomeProperty was used during deserialization
Assert.IsTrue(root2.SomeProperty Is root.SomeProperty)
Assert.IsTrue(json.Equals("{""SomeProperty"":""My Name""}"))
Doing things this way eliminates the dependence of static serialization methods on runtime code that is present in the first solution. Now all name replacement logic is handled in runtime in one location.
Sample fiddle #2 here.
1
@Sascha - answer updated.
– dbc
Nov 7 at 20:11
add a comment |
2 Answers
2
active
oldest
votes
2 Answers
2
active
oldest
votes
active
oldest
votes
active
oldest
votes
up vote
1
down vote
As requested by @doom87er I will share the code, that worked for me. The solution is based on the comment by @dbc, with some changes. Please treat below code more like a conceptual code: I had to change some of the names and leave out some logik, that isn't required for this proof of concept. So there might be typos in it.
The main solution is to subclass the DefaultContractResolver and add the the cache-dictionary to that class. Something like this:
Public Class CacheContractResolver
Inherits DefaultContractResolver
Public Cache As Dictionary(of string, SomePropertyClass)
Public Sub New(preFilledCache As Dictionary(of string, SomePropertyClass)
Me.Cache = preFilledCache
End Sub
End Class
Then you pass the custom contract resolver using the JsonSerializerSettings like this:
Dim settings = New JsonSerializerSettings
settings.ContractResolver = New SupportControllerContractResolver(prefilledCache)
Dim result = JsonConvert.DeserializeObject(Of ParentObject)(jsonData, settings)
where prefilledCache is an instance of a dictionary containing the SomePropertyClass-objects.
The last step is to retrieve the cache in my JsonConverter's ReadJson function (that I attached to the SomeProperty as shown in the original post's sample code):
Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object
Dim cacheResolver = TryCast(serializer.ContractResolver, CacheContractResolver)
if cacheResolver is nothing return nothing ' add some better null handling here
Dim value As String = reader.Value.ToString().Trim()
Dim cachedObject as SomePropertyClass = nothing
if cacheResolver.Cache.TryGetValue(value, cachedObject) then return cachedObject
retun Nothing ' or new SomePropertyClass(value)
End Function
I tried it and it seems to work.
So in a nut shell:
- Subclass the DefaultContractResolver and include all the additional data, that you need.
- Pass an instance of you custom contract resolver with the additional data in the JsonSerializerSettings.
- In your JsonConverter trycast the passed contract resolver back to your custom contract resolver and there you have your additional data.
I would be happy if you comment on any catches that I might miss, but I think this should be solution that I can live with.
Thanks for you coments and help.
Sascha
Thanks. I was having Stack overflow issues with some really complex converters. Hopefully I can sort something out with this
– doom87er
Nov 7 at 20:54
add a comment |
up vote
1
down vote
As requested by @doom87er I will share the code, that worked for me. The solution is based on the comment by @dbc, with some changes. Please treat below code more like a conceptual code: I had to change some of the names and leave out some logik, that isn't required for this proof of concept. So there might be typos in it.
The main solution is to subclass the DefaultContractResolver and add the the cache-dictionary to that class. Something like this:
Public Class CacheContractResolver
Inherits DefaultContractResolver
Public Cache As Dictionary(of string, SomePropertyClass)
Public Sub New(preFilledCache As Dictionary(of string, SomePropertyClass)
Me.Cache = preFilledCache
End Sub
End Class
Then you pass the custom contract resolver using the JsonSerializerSettings like this:
Dim settings = New JsonSerializerSettings
settings.ContractResolver = New SupportControllerContractResolver(prefilledCache)
Dim result = JsonConvert.DeserializeObject(Of ParentObject)(jsonData, settings)
where prefilledCache is an instance of a dictionary containing the SomePropertyClass-objects.
The last step is to retrieve the cache in my JsonConverter's ReadJson function (that I attached to the SomeProperty as shown in the original post's sample code):
Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object
Dim cacheResolver = TryCast(serializer.ContractResolver, CacheContractResolver)
if cacheResolver is nothing return nothing ' add some better null handling here
Dim value As String = reader.Value.ToString().Trim()
Dim cachedObject as SomePropertyClass = nothing
if cacheResolver.Cache.TryGetValue(value, cachedObject) then return cachedObject
retun Nothing ' or new SomePropertyClass(value)
End Function
I tried it and it seems to work.
So in a nut shell:
- Subclass the DefaultContractResolver and include all the additional data, that you need.
- Pass an instance of you custom contract resolver with the additional data in the JsonSerializerSettings.
- In your JsonConverter trycast the passed contract resolver back to your custom contract resolver and there you have your additional data.
I would be happy if you comment on any catches that I might miss, but I think this should be solution that I can live with.
Thanks for you coments and help.
Sascha
Thanks. I was having Stack overflow issues with some really complex converters. Hopefully I can sort something out with this
– doom87er
Nov 7 at 20:54
add a comment |
up vote
1
down vote
up vote
1
down vote
As requested by @doom87er I will share the code, that worked for me. The solution is based on the comment by @dbc, with some changes. Please treat below code more like a conceptual code: I had to change some of the names and leave out some logik, that isn't required for this proof of concept. So there might be typos in it.
The main solution is to subclass the DefaultContractResolver and add the the cache-dictionary to that class. Something like this:
Public Class CacheContractResolver
Inherits DefaultContractResolver
Public Cache As Dictionary(of string, SomePropertyClass)
Public Sub New(preFilledCache As Dictionary(of string, SomePropertyClass)
Me.Cache = preFilledCache
End Sub
End Class
Then you pass the custom contract resolver using the JsonSerializerSettings like this:
Dim settings = New JsonSerializerSettings
settings.ContractResolver = New SupportControllerContractResolver(prefilledCache)
Dim result = JsonConvert.DeserializeObject(Of ParentObject)(jsonData, settings)
where prefilledCache is an instance of a dictionary containing the SomePropertyClass-objects.
The last step is to retrieve the cache in my JsonConverter's ReadJson function (that I attached to the SomeProperty as shown in the original post's sample code):
Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object
Dim cacheResolver = TryCast(serializer.ContractResolver, CacheContractResolver)
if cacheResolver is nothing return nothing ' add some better null handling here
Dim value As String = reader.Value.ToString().Trim()
Dim cachedObject as SomePropertyClass = nothing
if cacheResolver.Cache.TryGetValue(value, cachedObject) then return cachedObject
retun Nothing ' or new SomePropertyClass(value)
End Function
I tried it and it seems to work.
So in a nut shell:
- Subclass the DefaultContractResolver and include all the additional data, that you need.
- Pass an instance of you custom contract resolver with the additional data in the JsonSerializerSettings.
- In your JsonConverter trycast the passed contract resolver back to your custom contract resolver and there you have your additional data.
I would be happy if you comment on any catches that I might miss, but I think this should be solution that I can live with.
Thanks for you coments and help.
Sascha
As requested by @doom87er I will share the code, that worked for me. The solution is based on the comment by @dbc, with some changes. Please treat below code more like a conceptual code: I had to change some of the names and leave out some logik, that isn't required for this proof of concept. So there might be typos in it.
The main solution is to subclass the DefaultContractResolver and add the the cache-dictionary to that class. Something like this:
Public Class CacheContractResolver
Inherits DefaultContractResolver
Public Cache As Dictionary(of string, SomePropertyClass)
Public Sub New(preFilledCache As Dictionary(of string, SomePropertyClass)
Me.Cache = preFilledCache
End Sub
End Class
Then you pass the custom contract resolver using the JsonSerializerSettings like this:
Dim settings = New JsonSerializerSettings
settings.ContractResolver = New SupportControllerContractResolver(prefilledCache)
Dim result = JsonConvert.DeserializeObject(Of ParentObject)(jsonData, settings)
where prefilledCache is an instance of a dictionary containing the SomePropertyClass-objects.
The last step is to retrieve the cache in my JsonConverter's ReadJson function (that I attached to the SomeProperty as shown in the original post's sample code):
Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object
Dim cacheResolver = TryCast(serializer.ContractResolver, CacheContractResolver)
if cacheResolver is nothing return nothing ' add some better null handling here
Dim value As String = reader.Value.ToString().Trim()
Dim cachedObject as SomePropertyClass = nothing
if cacheResolver.Cache.TryGetValue(value, cachedObject) then return cachedObject
retun Nothing ' or new SomePropertyClass(value)
End Function
I tried it and it seems to work.
So in a nut shell:
- Subclass the DefaultContractResolver and include all the additional data, that you need.
- Pass an instance of you custom contract resolver with the additional data in the JsonSerializerSettings.
- In your JsonConverter trycast the passed contract resolver back to your custom contract resolver and there you have your additional data.
I would be happy if you comment on any catches that I might miss, but I think this should be solution that I can live with.
Thanks for you coments and help.
Sascha
edited Nov 7 at 18:56
answered Nov 7 at 18:19
Sascha
91111127
91111127
Thanks. I was having Stack overflow issues with some really complex converters. Hopefully I can sort something out with this
– doom87er
Nov 7 at 20:54
add a comment |
Thanks. I was having Stack overflow issues with some really complex converters. Hopefully I can sort something out with this
– doom87er
Nov 7 at 20:54
Thanks. I was having Stack overflow issues with some really complex converters. Hopefully I can sort something out with this
– doom87er
Nov 7 at 20:54
Thanks. I was having Stack overflow issues with some really complex converters. Hopefully I can sort something out with this
– doom87er
Nov 7 at 20:54
add a comment |
up vote
1
down vote
You can pass additional data to your custom JsonConverter
using StreamingContext.Context
via JsonSerializer.Context
. Using this mechanism it becomes possible to map class instances to names in a generic manner.
First, define the following interfaces and generic converter:
Public Interface ISerializationContext
Function TryGetNameTable(Of T)(ByRef table as INameTable(Of T)) as Boolean
End Interface
Public Interface INameTable(Of T)
Function TryGetName(value As T, ByRef name as String) As Boolean
Function TryGetValue(name as String, ByRef value as T) As Boolean
End Interface
Public Class NameTable(Of T) : Implements INameTable(Of T)
Public Property Dictionary as Dictionary(Of String, T) = New Dictionary(Of String, T)()
Public Property ReverseDictionary as Dictionary(Of T, String) = New Dictionary(Of T, String)()
Public Function Add(value as T, name as String) as T
Dictionary.Add(name, value)
ReverseDictionary.Add(value, name)
Return value
End Function
Public Function TryGetName(value As T, ByRef name as String) As Boolean Implements INameTable(Of T).TryGetName
Return ReverseDictionary.TryGetValue(value, name)
End Function
Function TryGetValue(name as String, ByRef value as T) As Boolean Implements INameTable(Of T).TryGetValue
Return Dictionary.TryGetValue(name, value)
End Function
End Class
Public Class ObjectToNameConverter(Of T)
Inherits JsonConverter
Public Overrides Function CanConvert(objectType As Type) As Boolean
Return GetType(T) = objectType
End Function
Public Overrides Sub WriteJson(writer As JsonWriter, value As Object, serializer As JsonSerializer)
Dim tValue = CType(value, T)
Dim context as ISerializationContext = CType(serializer.Context.Context, ISerializationContext)
If context Is Nothing
Throw New JsonSerializationException("No ISerializationContext.")
End If
Dim nameTable as INameTable(Of T) = Nothing
If (Not context.TryGetNameTable(Of T)(nameTable))
Throw New JsonSerializationException("No NameTable.")
End If
Dim name as String = Nothing
if (Not nameTable.TryGetName(tValue, name))
Throw New JsonSerializationException("No Name.")
End If
writer.WriteValue(name)
End Sub
Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object
Dim context as ISerializationContext = CType(serializer.Context.Context, ISerializationContext)
If context Is Nothing
Throw New JsonSerializationException("No ISerializationContext.")
End If
Dim nameTable as INameTable(Of T) = Nothing
If (Not context.TryGetNameTable(Of T)(nameTable))
Throw New JsonSerializationException("No NameTable.")
End If
Dim name As String = serializer.Deserialize(Of String)(reader)
If name Is Nothing Then
Return Nothing
End If
dim tValue as T = Nothing
nameTable.TryGetValue(name, tValue)
return tValue
End Function
End Class
Next, define the following concrete implementations:
Public Class RootObject
<JsonConverter(GetType(ObjectToNameConverter(Of SomePropertyClass)))> _
Public Property SomeProperty As SomePropertyClass
End Class
Public Class SomePropertyClass
End Class
Public Class MySerializationContext : Implements ISerializationContext
Public Function Add(value as SomePropertyClass, name as String) as SomePropertyClass
Return SomePropertyNameTable.Add(value, name)
End Function
Property SomePropertyNameTable as NameTable(Of SomePropertyClass) = New NameTable(Of SomePropertyClass)
Public Function TryGetNameTable(Of T)(ByRef table as INameTable(Of T)) as Boolean Implements ISerializationContext.TryGetNameTable
if (GetType(T) Is GetType(SomePropertyClass))
table = SomePropertyNameTable
return True
End If
table = Nothing
return False
End Function
End Class
Now, you can replace instances of SomePropertyClass
with their names during deserialization as follows:
Dim context as MySerializationContext = New MySerializationContext()
Dim someProperty as SomePropertyClass = context.Add(New SomePropertyClass(), "My Name")
Dim root as RootObject = New RootObject With { .SomeProperty = someProperty }
Dim settings = new JsonSerializerSettings With _
{ _
.Context = New System.Runtime.Serialization.StreamingContext(System.Runtime.Serialization.StreamingContextStates.All, context)
}
Dim json as String = JsonConvert.SerializeObject(root, settings)
Console.WriteLine(json) ' Prints {"SomeProperty":"My Name"}
dim root2 as RootObject = JsonConvert.DeserializeObject(Of RootObject)(json, settings)
' Assert that the same instance of SomeProperty was used during deserialization
Assert.IsTrue(root2.SomeProperty Is root.SomeProperty)
Assert.IsTrue(json.Equals("{""SomeProperty"":""My Name""}"))
Notes:
ISerializationContext.TryGetNameTable(Of T)(ByRef table as INameTable(Of T))
is generic so that object-to-name replacement can be supported for multiple types of objects simultaneously without the converters interfering with each other.
The concrete implementation need not be so generic, however. Here
MySerializationContext
only supports name replacement for instances ofSomePropertyClass
. Others could be added as needed.
As stated in Does Json.NET cache types' serialization information?, Newtonsoft recommends caching instances of
DefaultContractResolver
and its subtypes for best performance. Thus it may be preferable to pass additional data viaStreamingContext.Context
rather than via freshly allocated instances of subclasses ofDefaultContractResolver
.
Sample working .Net fiddle #1 here.
As an alternative, while the design above works, in my opinion it would be simpler to remove <JsonConverter(GetType(ObjectToNameConverter(Of SomePropertyClass)))>
from SomeProperty
and instead pass an appropriately initialized ObjectToNameConverter(Of SomePropertyClass)
, containing a local reference to some INameTable(Of SomePropertyClass)
, in JsonSerializerSettings.Converters
.
Define the converter and interfaces like so. Notice that ObjectToNameConverter(Of T)
now has a parameterized constructor and that ISerializationContext
is no longer required:
Public Interface INameTable(Of T)
Function TryGetName(value As T, ByRef name as String) As Boolean
Function TryGetValue(name as String, ByRef value as T) As Boolean
End Interface
Public Class NameTable(Of T) : Implements INameTable(Of T)
Public Property Dictionary as Dictionary(Of String, T) = New Dictionary(Of String, T)()
Public Property ReverseDictionary as Dictionary(Of T, String) = New Dictionary(Of T, String)()
Public Function Add(value as T, name as String) as T
Dictionary.Add(name, value)
ReverseDictionary.Add(value, name)
Return value
End Function
Public Function TryGetName(value As T, ByRef name as String) As Boolean Implements INameTable(Of T).TryGetName
Return ReverseDictionary.TryGetValue(value, name)
End Function
Function TryGetValue(name as String, ByRef value as T) As Boolean Implements INameTable(Of T).TryGetValue
Return Dictionary.TryGetValue(name, value)
End Function
End Class
Public Class ObjectToNameConverter(Of T)
Inherits JsonConverter
Private Property NameTable as INameTable(Of T)
Public Sub New(nameTable as INameTable(Of T))
If nameTable Is Nothing
Throw new ArgumentNullException("nameTable")
End If
Me.NameTable = nameTable
End Sub
Public Overrides Function CanConvert(objectType As Type) As Boolean
Return GetType(T) = objectType
End Function
Public Overrides Sub WriteJson(writer As JsonWriter, value As Object, serializer As JsonSerializer)
Dim tValue = CType(value, T)
Dim name as String = Nothing
if (Not NameTable.TryGetName(tValue, name))
Throw New JsonSerializationException("No Name.")
End If
writer.WriteValue(name)
End Sub
Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object
Dim name As String = serializer.Deserialize(Of String)(reader)
If name Is Nothing Then
Return Nothing
End If
dim tValue as T = Nothing
NameTable.TryGetValue(name, tValue)
return tValue
End Function
End Class
Then serialize as follows:
dim nameTable = new NameTable(Of SomePropertyClass)()
Dim someProperty as SomePropertyClass = nameTable.Add(New SomePropertyClass(), "My Name")
Dim root as RootObject = New RootObject With { .SomeProperty = someProperty }
Dim settings = new JsonSerializerSettings()
settings.Converters.Add(new ObjectToNameConverter(Of SomePropertyClass)(nameTable))
Dim json as String = JsonConvert.SerializeObject(root, settings)
Console.WriteLine(json) ' Prints {"SomeProperty":"My Name"}
dim root2 as RootObject = JsonConvert.DeserializeObject(Of RootObject)(json, settings)
' Assert that the same instance of SomeProperty was used during deserialization
Assert.IsTrue(root2.SomeProperty Is root.SomeProperty)
Assert.IsTrue(json.Equals("{""SomeProperty"":""My Name""}"))
Doing things this way eliminates the dependence of static serialization methods on runtime code that is present in the first solution. Now all name replacement logic is handled in runtime in one location.
Sample fiddle #2 here.
1
@Sascha - answer updated.
– dbc
Nov 7 at 20:11
add a comment |
up vote
1
down vote
You can pass additional data to your custom JsonConverter
using StreamingContext.Context
via JsonSerializer.Context
. Using this mechanism it becomes possible to map class instances to names in a generic manner.
First, define the following interfaces and generic converter:
Public Interface ISerializationContext
Function TryGetNameTable(Of T)(ByRef table as INameTable(Of T)) as Boolean
End Interface
Public Interface INameTable(Of T)
Function TryGetName(value As T, ByRef name as String) As Boolean
Function TryGetValue(name as String, ByRef value as T) As Boolean
End Interface
Public Class NameTable(Of T) : Implements INameTable(Of T)
Public Property Dictionary as Dictionary(Of String, T) = New Dictionary(Of String, T)()
Public Property ReverseDictionary as Dictionary(Of T, String) = New Dictionary(Of T, String)()
Public Function Add(value as T, name as String) as T
Dictionary.Add(name, value)
ReverseDictionary.Add(value, name)
Return value
End Function
Public Function TryGetName(value As T, ByRef name as String) As Boolean Implements INameTable(Of T).TryGetName
Return ReverseDictionary.TryGetValue(value, name)
End Function
Function TryGetValue(name as String, ByRef value as T) As Boolean Implements INameTable(Of T).TryGetValue
Return Dictionary.TryGetValue(name, value)
End Function
End Class
Public Class ObjectToNameConverter(Of T)
Inherits JsonConverter
Public Overrides Function CanConvert(objectType As Type) As Boolean
Return GetType(T) = objectType
End Function
Public Overrides Sub WriteJson(writer As JsonWriter, value As Object, serializer As JsonSerializer)
Dim tValue = CType(value, T)
Dim context as ISerializationContext = CType(serializer.Context.Context, ISerializationContext)
If context Is Nothing
Throw New JsonSerializationException("No ISerializationContext.")
End If
Dim nameTable as INameTable(Of T) = Nothing
If (Not context.TryGetNameTable(Of T)(nameTable))
Throw New JsonSerializationException("No NameTable.")
End If
Dim name as String = Nothing
if (Not nameTable.TryGetName(tValue, name))
Throw New JsonSerializationException("No Name.")
End If
writer.WriteValue(name)
End Sub
Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object
Dim context as ISerializationContext = CType(serializer.Context.Context, ISerializationContext)
If context Is Nothing
Throw New JsonSerializationException("No ISerializationContext.")
End If
Dim nameTable as INameTable(Of T) = Nothing
If (Not context.TryGetNameTable(Of T)(nameTable))
Throw New JsonSerializationException("No NameTable.")
End If
Dim name As String = serializer.Deserialize(Of String)(reader)
If name Is Nothing Then
Return Nothing
End If
dim tValue as T = Nothing
nameTable.TryGetValue(name, tValue)
return tValue
End Function
End Class
Next, define the following concrete implementations:
Public Class RootObject
<JsonConverter(GetType(ObjectToNameConverter(Of SomePropertyClass)))> _
Public Property SomeProperty As SomePropertyClass
End Class
Public Class SomePropertyClass
End Class
Public Class MySerializationContext : Implements ISerializationContext
Public Function Add(value as SomePropertyClass, name as String) as SomePropertyClass
Return SomePropertyNameTable.Add(value, name)
End Function
Property SomePropertyNameTable as NameTable(Of SomePropertyClass) = New NameTable(Of SomePropertyClass)
Public Function TryGetNameTable(Of T)(ByRef table as INameTable(Of T)) as Boolean Implements ISerializationContext.TryGetNameTable
if (GetType(T) Is GetType(SomePropertyClass))
table = SomePropertyNameTable
return True
End If
table = Nothing
return False
End Function
End Class
Now, you can replace instances of SomePropertyClass
with their names during deserialization as follows:
Dim context as MySerializationContext = New MySerializationContext()
Dim someProperty as SomePropertyClass = context.Add(New SomePropertyClass(), "My Name")
Dim root as RootObject = New RootObject With { .SomeProperty = someProperty }
Dim settings = new JsonSerializerSettings With _
{ _
.Context = New System.Runtime.Serialization.StreamingContext(System.Runtime.Serialization.StreamingContextStates.All, context)
}
Dim json as String = JsonConvert.SerializeObject(root, settings)
Console.WriteLine(json) ' Prints {"SomeProperty":"My Name"}
dim root2 as RootObject = JsonConvert.DeserializeObject(Of RootObject)(json, settings)
' Assert that the same instance of SomeProperty was used during deserialization
Assert.IsTrue(root2.SomeProperty Is root.SomeProperty)
Assert.IsTrue(json.Equals("{""SomeProperty"":""My Name""}"))
Notes:
ISerializationContext.TryGetNameTable(Of T)(ByRef table as INameTable(Of T))
is generic so that object-to-name replacement can be supported for multiple types of objects simultaneously without the converters interfering with each other.
The concrete implementation need not be so generic, however. Here
MySerializationContext
only supports name replacement for instances ofSomePropertyClass
. Others could be added as needed.
As stated in Does Json.NET cache types' serialization information?, Newtonsoft recommends caching instances of
DefaultContractResolver
and its subtypes for best performance. Thus it may be preferable to pass additional data viaStreamingContext.Context
rather than via freshly allocated instances of subclasses ofDefaultContractResolver
.
Sample working .Net fiddle #1 here.
As an alternative, while the design above works, in my opinion it would be simpler to remove <JsonConverter(GetType(ObjectToNameConverter(Of SomePropertyClass)))>
from SomeProperty
and instead pass an appropriately initialized ObjectToNameConverter(Of SomePropertyClass)
, containing a local reference to some INameTable(Of SomePropertyClass)
, in JsonSerializerSettings.Converters
.
Define the converter and interfaces like so. Notice that ObjectToNameConverter(Of T)
now has a parameterized constructor and that ISerializationContext
is no longer required:
Public Interface INameTable(Of T)
Function TryGetName(value As T, ByRef name as String) As Boolean
Function TryGetValue(name as String, ByRef value as T) As Boolean
End Interface
Public Class NameTable(Of T) : Implements INameTable(Of T)
Public Property Dictionary as Dictionary(Of String, T) = New Dictionary(Of String, T)()
Public Property ReverseDictionary as Dictionary(Of T, String) = New Dictionary(Of T, String)()
Public Function Add(value as T, name as String) as T
Dictionary.Add(name, value)
ReverseDictionary.Add(value, name)
Return value
End Function
Public Function TryGetName(value As T, ByRef name as String) As Boolean Implements INameTable(Of T).TryGetName
Return ReverseDictionary.TryGetValue(value, name)
End Function
Function TryGetValue(name as String, ByRef value as T) As Boolean Implements INameTable(Of T).TryGetValue
Return Dictionary.TryGetValue(name, value)
End Function
End Class
Public Class ObjectToNameConverter(Of T)
Inherits JsonConverter
Private Property NameTable as INameTable(Of T)
Public Sub New(nameTable as INameTable(Of T))
If nameTable Is Nothing
Throw new ArgumentNullException("nameTable")
End If
Me.NameTable = nameTable
End Sub
Public Overrides Function CanConvert(objectType As Type) As Boolean
Return GetType(T) = objectType
End Function
Public Overrides Sub WriteJson(writer As JsonWriter, value As Object, serializer As JsonSerializer)
Dim tValue = CType(value, T)
Dim name as String = Nothing
if (Not NameTable.TryGetName(tValue, name))
Throw New JsonSerializationException("No Name.")
End If
writer.WriteValue(name)
End Sub
Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object
Dim name As String = serializer.Deserialize(Of String)(reader)
If name Is Nothing Then
Return Nothing
End If
dim tValue as T = Nothing
NameTable.TryGetValue(name, tValue)
return tValue
End Function
End Class
Then serialize as follows:
dim nameTable = new NameTable(Of SomePropertyClass)()
Dim someProperty as SomePropertyClass = nameTable.Add(New SomePropertyClass(), "My Name")
Dim root as RootObject = New RootObject With { .SomeProperty = someProperty }
Dim settings = new JsonSerializerSettings()
settings.Converters.Add(new ObjectToNameConverter(Of SomePropertyClass)(nameTable))
Dim json as String = JsonConvert.SerializeObject(root, settings)
Console.WriteLine(json) ' Prints {"SomeProperty":"My Name"}
dim root2 as RootObject = JsonConvert.DeserializeObject(Of RootObject)(json, settings)
' Assert that the same instance of SomeProperty was used during deserialization
Assert.IsTrue(root2.SomeProperty Is root.SomeProperty)
Assert.IsTrue(json.Equals("{""SomeProperty"":""My Name""}"))
Doing things this way eliminates the dependence of static serialization methods on runtime code that is present in the first solution. Now all name replacement logic is handled in runtime in one location.
Sample fiddle #2 here.
1
@Sascha - answer updated.
– dbc
Nov 7 at 20:11
add a comment |
up vote
1
down vote
up vote
1
down vote
You can pass additional data to your custom JsonConverter
using StreamingContext.Context
via JsonSerializer.Context
. Using this mechanism it becomes possible to map class instances to names in a generic manner.
First, define the following interfaces and generic converter:
Public Interface ISerializationContext
Function TryGetNameTable(Of T)(ByRef table as INameTable(Of T)) as Boolean
End Interface
Public Interface INameTable(Of T)
Function TryGetName(value As T, ByRef name as String) As Boolean
Function TryGetValue(name as String, ByRef value as T) As Boolean
End Interface
Public Class NameTable(Of T) : Implements INameTable(Of T)
Public Property Dictionary as Dictionary(Of String, T) = New Dictionary(Of String, T)()
Public Property ReverseDictionary as Dictionary(Of T, String) = New Dictionary(Of T, String)()
Public Function Add(value as T, name as String) as T
Dictionary.Add(name, value)
ReverseDictionary.Add(value, name)
Return value
End Function
Public Function TryGetName(value As T, ByRef name as String) As Boolean Implements INameTable(Of T).TryGetName
Return ReverseDictionary.TryGetValue(value, name)
End Function
Function TryGetValue(name as String, ByRef value as T) As Boolean Implements INameTable(Of T).TryGetValue
Return Dictionary.TryGetValue(name, value)
End Function
End Class
Public Class ObjectToNameConverter(Of T)
Inherits JsonConverter
Public Overrides Function CanConvert(objectType As Type) As Boolean
Return GetType(T) = objectType
End Function
Public Overrides Sub WriteJson(writer As JsonWriter, value As Object, serializer As JsonSerializer)
Dim tValue = CType(value, T)
Dim context as ISerializationContext = CType(serializer.Context.Context, ISerializationContext)
If context Is Nothing
Throw New JsonSerializationException("No ISerializationContext.")
End If
Dim nameTable as INameTable(Of T) = Nothing
If (Not context.TryGetNameTable(Of T)(nameTable))
Throw New JsonSerializationException("No NameTable.")
End If
Dim name as String = Nothing
if (Not nameTable.TryGetName(tValue, name))
Throw New JsonSerializationException("No Name.")
End If
writer.WriteValue(name)
End Sub
Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object
Dim context as ISerializationContext = CType(serializer.Context.Context, ISerializationContext)
If context Is Nothing
Throw New JsonSerializationException("No ISerializationContext.")
End If
Dim nameTable as INameTable(Of T) = Nothing
If (Not context.TryGetNameTable(Of T)(nameTable))
Throw New JsonSerializationException("No NameTable.")
End If
Dim name As String = serializer.Deserialize(Of String)(reader)
If name Is Nothing Then
Return Nothing
End If
dim tValue as T = Nothing
nameTable.TryGetValue(name, tValue)
return tValue
End Function
End Class
Next, define the following concrete implementations:
Public Class RootObject
<JsonConverter(GetType(ObjectToNameConverter(Of SomePropertyClass)))> _
Public Property SomeProperty As SomePropertyClass
End Class
Public Class SomePropertyClass
End Class
Public Class MySerializationContext : Implements ISerializationContext
Public Function Add(value as SomePropertyClass, name as String) as SomePropertyClass
Return SomePropertyNameTable.Add(value, name)
End Function
Property SomePropertyNameTable as NameTable(Of SomePropertyClass) = New NameTable(Of SomePropertyClass)
Public Function TryGetNameTable(Of T)(ByRef table as INameTable(Of T)) as Boolean Implements ISerializationContext.TryGetNameTable
if (GetType(T) Is GetType(SomePropertyClass))
table = SomePropertyNameTable
return True
End If
table = Nothing
return False
End Function
End Class
Now, you can replace instances of SomePropertyClass
with their names during deserialization as follows:
Dim context as MySerializationContext = New MySerializationContext()
Dim someProperty as SomePropertyClass = context.Add(New SomePropertyClass(), "My Name")
Dim root as RootObject = New RootObject With { .SomeProperty = someProperty }
Dim settings = new JsonSerializerSettings With _
{ _
.Context = New System.Runtime.Serialization.StreamingContext(System.Runtime.Serialization.StreamingContextStates.All, context)
}
Dim json as String = JsonConvert.SerializeObject(root, settings)
Console.WriteLine(json) ' Prints {"SomeProperty":"My Name"}
dim root2 as RootObject = JsonConvert.DeserializeObject(Of RootObject)(json, settings)
' Assert that the same instance of SomeProperty was used during deserialization
Assert.IsTrue(root2.SomeProperty Is root.SomeProperty)
Assert.IsTrue(json.Equals("{""SomeProperty"":""My Name""}"))
Notes:
ISerializationContext.TryGetNameTable(Of T)(ByRef table as INameTable(Of T))
is generic so that object-to-name replacement can be supported for multiple types of objects simultaneously without the converters interfering with each other.
The concrete implementation need not be so generic, however. Here
MySerializationContext
only supports name replacement for instances ofSomePropertyClass
. Others could be added as needed.
As stated in Does Json.NET cache types' serialization information?, Newtonsoft recommends caching instances of
DefaultContractResolver
and its subtypes for best performance. Thus it may be preferable to pass additional data viaStreamingContext.Context
rather than via freshly allocated instances of subclasses ofDefaultContractResolver
.
Sample working .Net fiddle #1 here.
As an alternative, while the design above works, in my opinion it would be simpler to remove <JsonConverter(GetType(ObjectToNameConverter(Of SomePropertyClass)))>
from SomeProperty
and instead pass an appropriately initialized ObjectToNameConverter(Of SomePropertyClass)
, containing a local reference to some INameTable(Of SomePropertyClass)
, in JsonSerializerSettings.Converters
.
Define the converter and interfaces like so. Notice that ObjectToNameConverter(Of T)
now has a parameterized constructor and that ISerializationContext
is no longer required:
Public Interface INameTable(Of T)
Function TryGetName(value As T, ByRef name as String) As Boolean
Function TryGetValue(name as String, ByRef value as T) As Boolean
End Interface
Public Class NameTable(Of T) : Implements INameTable(Of T)
Public Property Dictionary as Dictionary(Of String, T) = New Dictionary(Of String, T)()
Public Property ReverseDictionary as Dictionary(Of T, String) = New Dictionary(Of T, String)()
Public Function Add(value as T, name as String) as T
Dictionary.Add(name, value)
ReverseDictionary.Add(value, name)
Return value
End Function
Public Function TryGetName(value As T, ByRef name as String) As Boolean Implements INameTable(Of T).TryGetName
Return ReverseDictionary.TryGetValue(value, name)
End Function
Function TryGetValue(name as String, ByRef value as T) As Boolean Implements INameTable(Of T).TryGetValue
Return Dictionary.TryGetValue(name, value)
End Function
End Class
Public Class ObjectToNameConverter(Of T)
Inherits JsonConverter
Private Property NameTable as INameTable(Of T)
Public Sub New(nameTable as INameTable(Of T))
If nameTable Is Nothing
Throw new ArgumentNullException("nameTable")
End If
Me.NameTable = nameTable
End Sub
Public Overrides Function CanConvert(objectType As Type) As Boolean
Return GetType(T) = objectType
End Function
Public Overrides Sub WriteJson(writer As JsonWriter, value As Object, serializer As JsonSerializer)
Dim tValue = CType(value, T)
Dim name as String = Nothing
if (Not NameTable.TryGetName(tValue, name))
Throw New JsonSerializationException("No Name.")
End If
writer.WriteValue(name)
End Sub
Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object
Dim name As String = serializer.Deserialize(Of String)(reader)
If name Is Nothing Then
Return Nothing
End If
dim tValue as T = Nothing
NameTable.TryGetValue(name, tValue)
return tValue
End Function
End Class
Then serialize as follows:
dim nameTable = new NameTable(Of SomePropertyClass)()
Dim someProperty as SomePropertyClass = nameTable.Add(New SomePropertyClass(), "My Name")
Dim root as RootObject = New RootObject With { .SomeProperty = someProperty }
Dim settings = new JsonSerializerSettings()
settings.Converters.Add(new ObjectToNameConverter(Of SomePropertyClass)(nameTable))
Dim json as String = JsonConvert.SerializeObject(root, settings)
Console.WriteLine(json) ' Prints {"SomeProperty":"My Name"}
dim root2 as RootObject = JsonConvert.DeserializeObject(Of RootObject)(json, settings)
' Assert that the same instance of SomeProperty was used during deserialization
Assert.IsTrue(root2.SomeProperty Is root.SomeProperty)
Assert.IsTrue(json.Equals("{""SomeProperty"":""My Name""}"))
Doing things this way eliminates the dependence of static serialization methods on runtime code that is present in the first solution. Now all name replacement logic is handled in runtime in one location.
Sample fiddle #2 here.
You can pass additional data to your custom JsonConverter
using StreamingContext.Context
via JsonSerializer.Context
. Using this mechanism it becomes possible to map class instances to names in a generic manner.
First, define the following interfaces and generic converter:
Public Interface ISerializationContext
Function TryGetNameTable(Of T)(ByRef table as INameTable(Of T)) as Boolean
End Interface
Public Interface INameTable(Of T)
Function TryGetName(value As T, ByRef name as String) As Boolean
Function TryGetValue(name as String, ByRef value as T) As Boolean
End Interface
Public Class NameTable(Of T) : Implements INameTable(Of T)
Public Property Dictionary as Dictionary(Of String, T) = New Dictionary(Of String, T)()
Public Property ReverseDictionary as Dictionary(Of T, String) = New Dictionary(Of T, String)()
Public Function Add(value as T, name as String) as T
Dictionary.Add(name, value)
ReverseDictionary.Add(value, name)
Return value
End Function
Public Function TryGetName(value As T, ByRef name as String) As Boolean Implements INameTable(Of T).TryGetName
Return ReverseDictionary.TryGetValue(value, name)
End Function
Function TryGetValue(name as String, ByRef value as T) As Boolean Implements INameTable(Of T).TryGetValue
Return Dictionary.TryGetValue(name, value)
End Function
End Class
Public Class ObjectToNameConverter(Of T)
Inherits JsonConverter
Public Overrides Function CanConvert(objectType As Type) As Boolean
Return GetType(T) = objectType
End Function
Public Overrides Sub WriteJson(writer As JsonWriter, value As Object, serializer As JsonSerializer)
Dim tValue = CType(value, T)
Dim context as ISerializationContext = CType(serializer.Context.Context, ISerializationContext)
If context Is Nothing
Throw New JsonSerializationException("No ISerializationContext.")
End If
Dim nameTable as INameTable(Of T) = Nothing
If (Not context.TryGetNameTable(Of T)(nameTable))
Throw New JsonSerializationException("No NameTable.")
End If
Dim name as String = Nothing
if (Not nameTable.TryGetName(tValue, name))
Throw New JsonSerializationException("No Name.")
End If
writer.WriteValue(name)
End Sub
Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object
Dim context as ISerializationContext = CType(serializer.Context.Context, ISerializationContext)
If context Is Nothing
Throw New JsonSerializationException("No ISerializationContext.")
End If
Dim nameTable as INameTable(Of T) = Nothing
If (Not context.TryGetNameTable(Of T)(nameTable))
Throw New JsonSerializationException("No NameTable.")
End If
Dim name As String = serializer.Deserialize(Of String)(reader)
If name Is Nothing Then
Return Nothing
End If
dim tValue as T = Nothing
nameTable.TryGetValue(name, tValue)
return tValue
End Function
End Class
Next, define the following concrete implementations:
Public Class RootObject
<JsonConverter(GetType(ObjectToNameConverter(Of SomePropertyClass)))> _
Public Property SomeProperty As SomePropertyClass
End Class
Public Class SomePropertyClass
End Class
Public Class MySerializationContext : Implements ISerializationContext
Public Function Add(value as SomePropertyClass, name as String) as SomePropertyClass
Return SomePropertyNameTable.Add(value, name)
End Function
Property SomePropertyNameTable as NameTable(Of SomePropertyClass) = New NameTable(Of SomePropertyClass)
Public Function TryGetNameTable(Of T)(ByRef table as INameTable(Of T)) as Boolean Implements ISerializationContext.TryGetNameTable
if (GetType(T) Is GetType(SomePropertyClass))
table = SomePropertyNameTable
return True
End If
table = Nothing
return False
End Function
End Class
Now, you can replace instances of SomePropertyClass
with their names during deserialization as follows:
Dim context as MySerializationContext = New MySerializationContext()
Dim someProperty as SomePropertyClass = context.Add(New SomePropertyClass(), "My Name")
Dim root as RootObject = New RootObject With { .SomeProperty = someProperty }
Dim settings = new JsonSerializerSettings With _
{ _
.Context = New System.Runtime.Serialization.StreamingContext(System.Runtime.Serialization.StreamingContextStates.All, context)
}
Dim json as String = JsonConvert.SerializeObject(root, settings)
Console.WriteLine(json) ' Prints {"SomeProperty":"My Name"}
dim root2 as RootObject = JsonConvert.DeserializeObject(Of RootObject)(json, settings)
' Assert that the same instance of SomeProperty was used during deserialization
Assert.IsTrue(root2.SomeProperty Is root.SomeProperty)
Assert.IsTrue(json.Equals("{""SomeProperty"":""My Name""}"))
Notes:
ISerializationContext.TryGetNameTable(Of T)(ByRef table as INameTable(Of T))
is generic so that object-to-name replacement can be supported for multiple types of objects simultaneously without the converters interfering with each other.
The concrete implementation need not be so generic, however. Here
MySerializationContext
only supports name replacement for instances ofSomePropertyClass
. Others could be added as needed.
As stated in Does Json.NET cache types' serialization information?, Newtonsoft recommends caching instances of
DefaultContractResolver
and its subtypes for best performance. Thus it may be preferable to pass additional data viaStreamingContext.Context
rather than via freshly allocated instances of subclasses ofDefaultContractResolver
.
Sample working .Net fiddle #1 here.
As an alternative, while the design above works, in my opinion it would be simpler to remove <JsonConverter(GetType(ObjectToNameConverter(Of SomePropertyClass)))>
from SomeProperty
and instead pass an appropriately initialized ObjectToNameConverter(Of SomePropertyClass)
, containing a local reference to some INameTable(Of SomePropertyClass)
, in JsonSerializerSettings.Converters
.
Define the converter and interfaces like so. Notice that ObjectToNameConverter(Of T)
now has a parameterized constructor and that ISerializationContext
is no longer required:
Public Interface INameTable(Of T)
Function TryGetName(value As T, ByRef name as String) As Boolean
Function TryGetValue(name as String, ByRef value as T) As Boolean
End Interface
Public Class NameTable(Of T) : Implements INameTable(Of T)
Public Property Dictionary as Dictionary(Of String, T) = New Dictionary(Of String, T)()
Public Property ReverseDictionary as Dictionary(Of T, String) = New Dictionary(Of T, String)()
Public Function Add(value as T, name as String) as T
Dictionary.Add(name, value)
ReverseDictionary.Add(value, name)
Return value
End Function
Public Function TryGetName(value As T, ByRef name as String) As Boolean Implements INameTable(Of T).TryGetName
Return ReverseDictionary.TryGetValue(value, name)
End Function
Function TryGetValue(name as String, ByRef value as T) As Boolean Implements INameTable(Of T).TryGetValue
Return Dictionary.TryGetValue(name, value)
End Function
End Class
Public Class ObjectToNameConverter(Of T)
Inherits JsonConverter
Private Property NameTable as INameTable(Of T)
Public Sub New(nameTable as INameTable(Of T))
If nameTable Is Nothing
Throw new ArgumentNullException("nameTable")
End If
Me.NameTable = nameTable
End Sub
Public Overrides Function CanConvert(objectType As Type) As Boolean
Return GetType(T) = objectType
End Function
Public Overrides Sub WriteJson(writer As JsonWriter, value As Object, serializer As JsonSerializer)
Dim tValue = CType(value, T)
Dim name as String = Nothing
if (Not NameTable.TryGetName(tValue, name))
Throw New JsonSerializationException("No Name.")
End If
writer.WriteValue(name)
End Sub
Public Overrides Function ReadJson(reader As JsonReader, objectType As Type, existingValue As Object, serializer As JsonSerializer) As Object
Dim name As String = serializer.Deserialize(Of String)(reader)
If name Is Nothing Then
Return Nothing
End If
dim tValue as T = Nothing
NameTable.TryGetValue(name, tValue)
return tValue
End Function
End Class
Then serialize as follows:
dim nameTable = new NameTable(Of SomePropertyClass)()
Dim someProperty as SomePropertyClass = nameTable.Add(New SomePropertyClass(), "My Name")
Dim root as RootObject = New RootObject With { .SomeProperty = someProperty }
Dim settings = new JsonSerializerSettings()
settings.Converters.Add(new ObjectToNameConverter(Of SomePropertyClass)(nameTable))
Dim json as String = JsonConvert.SerializeObject(root, settings)
Console.WriteLine(json) ' Prints {"SomeProperty":"My Name"}
dim root2 as RootObject = JsonConvert.DeserializeObject(Of RootObject)(json, settings)
' Assert that the same instance of SomeProperty was used during deserialization
Assert.IsTrue(root2.SomeProperty Is root.SomeProperty)
Assert.IsTrue(json.Equals("{""SomeProperty"":""My Name""}"))
Doing things this way eliminates the dependence of static serialization methods on runtime code that is present in the first solution. Now all name replacement logic is handled in runtime in one location.
Sample fiddle #2 here.
edited Nov 7 at 20:09
answered Nov 7 at 19:51
dbc
52.3k765116
52.3k765116
1
@Sascha - answer updated.
– dbc
Nov 7 at 20:11
add a comment |
1
@Sascha - answer updated.
– dbc
Nov 7 at 20:11
1
1
@Sascha - answer updated.
– dbc
Nov 7 at 20:11
@Sascha - answer updated.
– dbc
Nov 7 at 20:11
add a comment |
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53193503%2fpass-additional-data-to-jsonconverter%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
@Amessihel: No, I don't think so, because the solution shows how deserialize to one class or another based on the json array. In my case there I know the class to deserialize my property to. I try to add a better example to the question, to make it clearer, what I want (that will take a few minutes).
– Sascha
Nov 7 at 16:36
1
Assuming you are constructing the serializer yourself, you can pass in additional data using
StreamingContext.Context
viaJsonSerializer.Context
. In JSON.NET how to get a reference to every deserialized object? shows an example.– dbc
Nov 7 at 16:37
@dbc That looks promising. I will try it and leave a comment on how it worked.
– Sascha
Nov 7 at 17:14
if it does work, could ya do me a favor and post the solution as an answer?
– doom87er
Nov 7 at 17:21
@doom87er -- working on one now.
– dbc
Nov 7 at 19:10