Search code examples
vb.netjson.netjson-deserialization

JSON.NET: Deserialization into an object with read-only properties yields unexpected results


I'm new to JSON.NET and I'm trying to deserialize a JSON string to a simple .NET object containing a read-only property.

The JSON I'm trying to deserialize contains this read-only property (what is "wrong" of course). The problem is, that another property now doesn't get the value given in the JSON-string but the value of the read-only property. I would have expected JSON.NET throwing an error or simply ignoring the read-only property, but never getting another property the value of the read-only property. Am I missing something?

Or to put it in other words: Why does the value of DeSerPerson.Communications.Items(1).ComValue change, when the return statement of getEMail is changed?

As a workaround: How can I tell the serializer to serialize getEmail but the deserializer to ignore it, in case it is present?

I created a fiddle here: https://dotnetfiddle.net/tUMQxF slightly modified after the first answer.

Imports System
Imports System.Collections.Generic
Imports Newtonsoft.Json

Public Class Communication

    Property ComType As Integer

    Property ComValue As String
End Class

Public Class Communications

    Private Property _fixedCommunication As New Communication With {.ComType = 2, .ComValue = "FixedEmail@web.de"}

    Public Property Items As New List(Of Communication)

    Public ReadOnly Property getEMail As Communication
        Get
            Return Items.Find(Function(x) x.ComType = 2) ' Yields unexpected result: DeSerPerson.Communications.Items(1).ComValue = Fred.Flintstone@web.de
        'Return _fixedCommunication ' Yields expected result: DeSerPerson.Communications.Items(1).ComValue = NewEmailAddress@web.de, though "error" would be a more intuitive result to me
        End Get
    End Property
End Class

Public Class Person

    Property Name As String

    Property Communications As New Communications
End Class

Public Module Deserialization

    Public Sub Main()
        'The following Json-String was the result of the following process:
        ' 1. Deserialization of Person with Email-Address Fred.Flintstone@web.de
        ' 2. Sent to javascript via WebMethod
        ' 3. In javascript the Email-Address was changed to NewEmailAddress@web.de
        ' 4. The whole object (including the read-only property) was sent back resulting in the following JSON:
        'Dim PersonJson = "
        '{
        '     'Name': 'Fred Flintstone'
        '    ,'Communications': {
        '        'Items':[
        '             {'ComType':1,'ComValue':'0711-4665'}
        '            ,{'ComType':2,'ComValue':'NewEmailAddress@web.de'}
        '            ]
        '        ,'getEMail':
        '            {'ComType':2,'ComValue':'Fred.Flintstone@web.de'}
        '    }
        '}".Replace("'", """")
        Dim PersonJson = "{'Name': 'Fred Flintstone','Communications':{'Items':[{'ComType':1,'ComValue':'0711-4665'},{'ComType':2,'ComValue':'NewEmailAddress@web.de'}],'getEMail':{'ComType':2,'ComValue':'Fred.Flintstone@web.de'}}}".Replace("'", """")
        Dim DeSerPerson = JsonConvert.DeserializeObject(Of Person)(PersonJson)
        Console.WriteLine("Result for DeSerPerson.Communications.Items(1).ComValue:")
        Console.WriteLine("--------------------------------------------------------")
        Console.WriteLine("Expected     : NewEmailAddress@web.de (or Error)")
        Console.WriteLine("but result is: " & DeSerPerson.Communications.Items(1).ComValue) ' Fred.Flintstone@web.de
    'Console.ReadLine()
    End Sub
End Module

Solution

  • The explanation is easy. The deserializer just fills the values in the existing object, not create a new object and assign it to your property.

    Note that the Readonly modifier states that the value of the property (ie, the reference to the object) can't be changed but not the properties of the Communication object itself.

    In this case, your getEmail property returns a reference to Items(1) because that is the item with ComType = 2.

    As to "ignore" the object when deserializing, you can easily use a clone method. This way, you return the same values from getEmail, but not the same object:

    Add the clone method to Communication:

    Public Class Communication
    
        Property ComType As Integer
    
        Property ComValue As String
    
        Public Function Clone() As Communication
            Return New Communication() With {.ComType = Me.ComType, .ComValue = Me.ComValue}
        End Function
    End Class
    

    Return the cloned object when reading the property:

    Public ReadOnly Property getEMail As Communication
        Get
            Return Items.Find(Function(x) x.ComType = 2).Clone() 
        End Get
    End Property
    

    Of course this is not an optimum code. It's just to show you how the thing works :)