3

I need to serialize any object (to xml, json, other - don't really care) for the purpose of transmitting/cloning etc.

I have found the newtonsoft.json library quite simple to use for this, except it doesn't serialize private members. I have found examples in c# that seem to offer what I need using contracts but I can't get them to work.

I would appreciate a simple example:

e.g.

 Class Person
     Private _NI as integer
     Public Name as string
End Class

I need two functions:

Function SerializePerson(P as person) as string

Function DeSerializePerson(SerializedText as string) as Person

Ideally, I would like an approach that would be easy to duplicate for any object.

Matt Wilko
  • 26,994
  • 10
  • 93
  • 143
Tim
  • 120
  • 1
  • 3
  • 8

3 Answers3

4

The trick with Newtonsoft is to changes the JsonSerializerSettings and pass them into JsonConvert.SerializeObject. This answer has the full details of this change: https://stackoverflow.com/a/24107081/2019162

Edit

I didn't think of the linked code being in C#, since I work in both so often. My apologies. I have translated the code below:

Dim settings As New JsonSerializerSettings() With {.ContractResolver = New MyContractResolver()}
Dim json As String = JsonConvert.SerializeObject(obj, settings)

And the contract resolver:

Imports System.Reflection
Imports Newtonsoft.Json
Imports Newtonsoft.Json.Serialization

Public Class MyContractResolver
    Inherits DefaultContractResolver
    Protected Overrides Function CreateProperties(type As Type, memberSerialization As MemberSerialization) As IList(Of JsonProperty)
        Dim props = type.GetProperties(BindingFlags.Public Or BindingFlags.NonPublic Or BindingFlags.Instance).
                            Select(Function(p) MyBase.CreateProperty(p, memberSerialization)).
                            Union(type.GetFields(BindingFlags.Public Or BindingFlags.NonPublic Or BindingFlags.Instance).
                            Select(Function(f) MyBase.CreateProperty(f, memberSerialization))).ToList()

        props.ForEach(Sub(p)
                          p.Writable = True
                          p.Readable = True
                      End Sub)
        Return props
    End Function
End Class

Most code converters change C# Lambda's into Function() in VB.NET, but if they need no return type they should be converted to Sub(). This was likely what was causing your issue.

Community
  • 1
  • 1
JoelC
  • 3,664
  • 9
  • 33
  • 38
  • This seems to be exactly what I need, but it doesn't compile when converted to VB. The props.ForEach(Function(p) throws an error - "Cannot infer a return type. Consider adding an 'As' clause.." – Tim Dec 16 '14 at 10:41
  • @Tim I have updated my answer with the correctly (I hope) converted VB.NET code. – JoelC Dec 16 '14 at 13:34
  • This is great - thanks a lot. You were right about the sub/function thing as well! – Tim Jan 09 '15 at 11:20
2

Using a DataContractSerializer you tag those things you want to serialize with the appropriate Attribute. You did not share what you tried or how it failed when you tried to use it, but here is how:

Imports System.Runtime.Serialization
Imports System.ComponentModel

<DataContract>
Public Class Employee
    <DataMember(Name:="Name")>
    Public Property Name As String

    <DataMember(Name:="Dept")>
    Public Property Dept As String

    <DataMember(Name:="Index")>
    Public Property Index As Integer

    <DataMember(Name:="secret")>
    Private secret As Integer

    ' most serializers require a simple ctor
    Public Sub New()

    End Sub

    Public Sub SetValue(n As Integer)
        secret = n
    End Sub

End Class

Use <DataContract> on the class and <Datamember> on the members. Then serializing:

' test data
Dim emp As New Employee With {.Name = "Ziggy", .Dept = "Manager", .Index = 2}
emp.SetValue(4)            ' set private member value

Dim ser = New DataContractSerializer(GetType(Employee))

Using fs As New FileStream("C:\Temp\DCTest.xml", FileMode.OpenOrCreate),
                      xw = XmlWriter.Create(fs)
    ser.WriteObject(xw, emp)
End Using

Output:

<?xml version="1.0" encoding="utf-8"?>
   <Employee xmlns:i="..." xmlns="...">
      <Dept>Manager</Dept>
      <Index>2</Index>
      <Name>Ziggy</Name>
      <secret>4</secret>
 </Employee>

As for one-size-fits-all functions for this type of thing, I personally tend to treat serialization as class or app or task specific (I also favor binary serialization over XML 100 to 1), but...

Private Function SerializeToString(Of T)(obj As T) As String
    ' there might be more efficient ways...
    ' this is off the top of my head
    Dim ser = New DataContractSerializer(GetType(T))
    Dim xml As String

    Using ms As New MemoryStream()
        ser.WriteObject(ms, obj)
        ms.Position = 0
        Using sr As New StreamReader(ms)
            xml = sr.ReadToEnd
        End Using
    End Using

    Return xml
End Function

Public Function DeserializeFromString(Of T)(xmlString As String) As T

    Dim ser = New DataContractSerializer(GetType(T))
    Dim ret As T

    Using sr As New StringReader(xmlString),
        ms As New MemoryStream(Encoding.Default.GetBytes(sr.ReadToEnd))
        ret = CType(ser.ReadObject(ms), T)
    End Using

    Return ret

End Function

Seems to work in a round trip test:

Dim emp As New Employee With {.Name = "Ziggy", .Dept = "Manager", .Index = 2}
emp.SetValue(4)

Dim str = SerializeToString(Of Employee)(emp)

Dim emp2 As Employee = DeserializeFromString(Of Employee)(str)

' serialize to disk (not shown)
If SerializeToFile("C:\Temp\DCTest.xml", emp) Then

    Dim emp3 As Employee = DeserializeFromFile(Of Employee)("C:\Temp\DCTest.xml")

End If

The deserialized version has the correct private, secret value:

enter image description here

Ňɏssa Pøngjǣrdenlarp
  • 38,411
  • 12
  • 59
  • 178
  • Thanks for this. I may end up with this as my preferred solution, but if the JsonConvert can be sorted that would be neater, not requiring extra decoration on my class's properties etc. – Tim Dec 16 '14 at 10:42
  • only DataContract serializers can access private members, some like ProtoBuf-NET have event based ways, but Contract serializers all rely on attributes. – Ňɏssa Pøngjǣrdenlarp Dec 16 '14 at 14:55
1

I tried this long ago, and found that the best solution was to use .NET inbuilt binary serialisation, it was most effective at preserving the internal class variables. Tactics such as XML serializaiton often failed with the complex classes I'd written, and when adding new members to the class I often had to update the methods I'd written to implement IXmlSerializable.

The context was that I needed to pass code from a web site into a web service - which was the only permitted way I could pass data from the web zone through our firewall to our internal business servers. I wished to pass the entire object data into the web service to run some business logic and do do the persist to database bit.

The solution was Binary serialization into a byte array - which is an allowable input type for a web service - and binary deserialization within the web service.

Example of how to serialize, given a web service that returns a boolean result for sucess and assuming the object you want to serialize is called myObject

    dim result as boolean
    Dim myStream As New System.IO.MemoryStream()
    Dim myFormatter As New System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
    myFormatter.Serialize(myStream, myObject)  '1. Serialise into stream

    Dim bytes As Byte() = New Byte(myStream.Length) {}
    myStream.Position = 0
    myStream.Read(bytes, 0, myStream.Length)  '2. read stream into byte array

    result = myWebService.PersistObject(bytes) 

and here's the deserialization bit at the other end, assuming the class of the object was called widget and the data was passed into the methos as a variable called objectBytes

    Dim myWidget As New Widget
    Dim myStream As New System.IO.MemoryStream()
    Dim myFormatter As New System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
    myStream.Write(objectBytes, 0, objectBytes.Length)
    myStream.Position = 0
    myWidget = myFormatter.Deserialize(myStream)

Note that for this to work, you must include the class libraries defining the class at the receiving end. But the cool thing is that it works for any class

Andy R
  • 31
  • 1
  • 4
  • Doesn't compile with Option Strict On. You also need to remove the call to myWebService for it to be useable and the target class also needs to be marked as Serializable. Oh and you are not disposing of your streams – Matt Wilko Dec 12 '14 at 16:34
  • Matt - you are absolutely right about the need for the Serializable attribute on the class, my apologies for forgetting it. The reference to myWebService- I just wanted to show the person asking the question an example of the code in context. You might notice I mentioned some assumptions about services and classes in the comment above the code. Similarly the streams - I was after posting a code snippet highlighting the methodology, not reproducing every bit of good practice. – Andy R Dec 15 '14 at 16:17
  • Thanks for this - it seems that it might do the trick, but I am leaning towards the Json solution at the moment since it seeems less work. – Tim Dec 16 '14 at 10:44