1

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.

dbc
  • 104,963
  • 20
  • 228
  • 340
Sascha
  • 1,210
  • 1
  • 17
  • 33
  • @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 07 '18 at 16:36
  • 1
    Assuming you are constructing the serializer yourself, you can pass in additional data using [`StreamingContext.Context`](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.serialization.streamingcontext.context?view=netframework-4.7.2#System_Runtime_Serialization_StreamingContext_Context) via [`JsonSerializer.Context`](https://www.newtonsoft.com/json/help/html/P_Newtonsoft_Json_JsonSerializer_Context.htm). [In JSON.NET how to get a reference to every deserialized object?](https://stackoverflow.com/a/39301378/3744182) shows an example. – dbc Nov 07 '18 at 16:37
  • @dbc That looks promising. I will try it and leave a comment on how it worked. – Sascha Nov 07 '18 at 17:14
  • if it does work, could ya do me a favor and post the solution as an answer? – doom87er Nov 07 '18 at 17:21
  • @doom87er -- working on one now. – dbc Nov 07 '18 at 19:10

2 Answers2

3

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 of SomePropertyClass. 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 via StreamingContext.Context rather than via freshly allocated instances of subclasses of DefaultContractResolver.

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.

dbc
  • 104,963
  • 20
  • 228
  • 340
1

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:

  1. Subclass the DefaultContractResolver and include all the additional data, that you need.
  2. Pass an instance of you custom contract resolver with the additional data in the JsonSerializerSettings.
  3. 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

Sascha
  • 1,210
  • 1
  • 17
  • 33
  • Thanks. I was having Stack overflow issues with some really complex converters. Hopefully I can sort something out with this – doom87er Nov 07 '18 at 20:54