1

I'm trying to deserialize a JSON string into a List using C# in Visual Studio for Mac for an Xamarin.Mac project.

I can serialize without any problem with this:

using (StreamWriter file = File.CreateText(filename))
            {
                var serializer = new JsonSerializer()
                {
                    Formatting = Formatting.Indented,
                    TypeNameHandling = TypeNameHandling.Auto,
                    TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple,
                    ReferenceLoopHandling = ReferenceLoopHandling.Ignore
                };
                await Task.Run(() => serializer.Serialize(file, contactroot));
            }

But I get the error above when I try to deserialize using this:

var contacts = JsonConvert.DeserializeObject<ContactRoot>(json);

ContactRoot class:

public class ContactRoot
{
    public List<Contact> contacts { get; set; }
}

The Contact class inherits the NSObject class as required for binding to controls in the Mac app. I'd include that too but there's over 70 properties with multiple attributes. Here's a couple of snippets from that class:

[Register("Contact")]
public class Contact : NSObject

... and a lot of properties like this:

public DateTime? updated_at { get; set; }

    [Export("updatedString"), Display(Name = "Updated Date"), Category("Misc"), Description("3"), DefaultValue(TypeOfValue.DateTime)]
    public string updatedString
    {
        get {
            if (this.updated_at.HasValue){
                return this.updated_at.Value.ToString();
            } else{
                return string.Empty;
            }
        }
        set
        {
            WillChangeValue("updatedString");
            try
            {
                if (!String.IsNullOrEmpty(value)) this.updated_at = Convert.ToDateTime(value);
            }
            catch (Exception)
            {
            }
            DidChangeValue("updatedString");
        }
    }

I'm using DateTime? because the API source requires that, and since Xamarin data binding doesn't seem to handle nulls for int or DateTime I'm using a string property to do the conversion. That works fine when using the app, but not sure if that's contributing to the error when deserializing. I'm new to developing for Mac, so I've probably missed something.

EDIT

Wow thanks for the quick responses! Prior to seeing any comments I tried a simpler class but got the same result:

[Register("SimpleContact")]
public class SimpleContact : NSObject
{
    int _id = 0;
    string _first = String.Empty;
    string _last = String.Empty;

    [Export("id")]
    public int id {
        get { return _id; }
        set {
            WillChangeValue("id");
            _id = value;
            DidChangeValue("id");
        }
    }

    [Export("first")]
    public string first {
        get { return _first; }
        set {
            WillChangeValue("first");
            _first = value;
            DidChangeValue("first");
        }
    }

    [Export("last")]
    public string last
    {
        get { return _last; }
        set
        {
            WillChangeValue("last");
            _last = value;
            DidChangeValue("last");
        }
    }

    [Export("fullname")]
    public string fullname
    {
        get { return this.first + " " + this.last; }
    }

    public SimpleContact()
    {}

    public SimpleContact(int id, string first, string last)
    {
        this.id = id;
        this.first = first;
        this.last = last;
    }

    public override string ToString()
    {
        return this.fullname;
    }
}

I will try the POCO approach next and post the results. Thank you so much!

EDIT Here's what looks like the meat of the exception. I don't think the rest of the exception would be of any use:

Crashed Thread:        0  tid_307  Dispatch queue: com.apple.main-thread

Exception Type:        EXC_CRASH (SIGABRT)
Exception Codes:       0x0000000000000000, 0x0000000000000000
Exception Note:        EXC_CORPSE_NOTIFY

External Modification Warnings:
Debugger attached to process.

Application Specific Information:
abort() called
WealthBox Addon(6016,0x11d9ae5c0) malloc: *** error for object 0x7fff7f110880: pointer being freed was not allocated


Thread 0 Crashed:: tid_307  Dispatch queue: com.apple.main-thread
0   libsystem_kernel.dylib          0x00007fff7f08223e __pthread_kill + 10
1   libsystem_pthread.dylib         0x00007fff7f138c1c pthread_kill + 285
2   libsystem_c.dylib               0x00007fff7efeb268 __abort + 144
3   libsystem_c.dylib               0x00007fff7efeb1d8 abort + 142
4   libsystem_malloc.dylib          0x00007fff7f0fa6e2 malloc_vreport + 545
5   libsystem_malloc.dylib          0x00007fff7f0fa4a3 malloc_report + 152
6   libsystem_malloc.dylib          0x00007fff7f0f2aff malloc_set_zone_name + 47
7   ???                             0x00000001162ad420 0 + 4666872864
8   ???                             0x000000011616c6a3 0 + 4665558691
9   ???                             0x0000000116275739 0 + 4666644281
10  ???                             0x0000000116265443 0 + 4666577987
11  ???                             0x000000011625cbd3 0 + 4666543059
12  ???                             0x000000011618480b 0 + 4665657355
13  ???                             0x00000001162755bb 0 + 4666643899
14  ???                             0x0000000116265443 0 + 4666577987
15  ???                             0x000000011625cbd3 0 + 4666543059
16  ???                             0x000000011618480b 0 + 4665657355
17  ???                             0x000000011618885b 0 + 4665673819
18  ???                             0x0000000116185c7b 0 + 4665662587
19  ???                             0x000000011618486b 0 + 4665657451
20  ???                             0x000000011609f303 0 + 4664718083
21  ???                             0x000000011609cc03 0 + 4664708099
22  ???                             0x000000011609c84e 0 + 4664707150
23  ???                             0x0000000116095663 0 + 4664677987
24  ???                             0x0000000116087a33 0 + 4664621619
25  Browning-Financial.WealthBox-Addon  0x000000010edc5593 mono_jit_runtime_invoke + 1443 (mini-runtime.c:2806)
26  Browning-Financial.WealthBox-Addon  0x000000010eeaee4f mono_runtime_invoke_checked + 127 (object.c:2887)
27  Browning-Financial.WealthBox-Addon  0x000000010eeb36fc mono_runtime_invoke + 76 (object.c:2941)
28  Browning-Financial.WealthBox-Addon  0x000000010ecadaa2 xamarin_invoke_trampoline + 6018 (trampolines-invoke.m:456)
29  Browning-Financial.WealthBox-Addon  0x000000010ecaec7d xamarin_arch_trampoline + 189 (trampolines-x86_64.m:547)
30  Browning-Financial.WealthBox-Addon  0x000000010ecb00b1 xamarin_x86_64_common_trampoline + 110
31  com.apple.AppKit                0x00007fff4f3deb5a -[NSViewController _sendViewDidLoad] + 97
32  com.apple.AppKit                0x00007fff4f3dbeb5 _noteLoadCompletionForObject + 645
33  com.apple.AppKit                0x00007fff4f24d99f -[NSIBObjectData nibInstantiateWithOwner:options:topLevelObjects:] + 2086
34  com.apple.AppKit                0x00007fff4f3e3114 -[NSNib _instantiateNibWithExternalNameTable:options:] + 679
35  com.apple.AppKit                0x00007fff4f3e2d70 -[NSNib _instantiateWithOwner:options:topLevelObjects:] + 136
36  com.apple.AppKit                0x00007fff4f3e1fdc -[NSViewController loadView] + 343
37  com.apple.AppKit                0x00007fff4f3ddb60 -[NSViewController _loadViewIfRequired] + 75
38  com.apple.AppKit                0x00007fff4f3ddacb -[NSViewController view] + 30
39  com.apple.AppKit                0x00007fff4f4d01da -[NSWindow _contentViewControllerChanged] + 109
40  com.apple.Foundation            0x00007fff540c7450 -[NSObject(NSKeyValueCoding) setValue:forKey:] + 331
41  com.apple.AppKit                0x00007fff4f3da987 -[NSWindow setValue:forKey:] + 111
42  com.apple.AppKit                0x00007fff4f32ce0c -[NSIBUserDefinedRuntimeAttributesConnector establishConnection] + 637
43  com.apple.AppKit                0x00007fff4f24d739 -[NSIBObjectData nibInstantiateWithOwner:options:topLevelObjects:] + 1472
44  com.apple.AppKit                0x00007fff4f3e3114 -[NSNib _instantiateNibWithExternalNameTable:options:] + 679
45  com.apple.AppKit                0x00007fff4f3e2d70 -[NSNib _instantiateWithOwner:options:topLevelObjects:] + 136
46  com.apple.AppKit                0x00007fff4fada64c -[NSStoryboard instantiateControllerWithIdentifier:] + 236
47  com.apple.AppKit                0x00007fff4f23e855 NSApplicationMain + 702
48  ???                             0x00000001151c56b7 0 + 4649146039
49  ???                             0x00000001151c53ab 0 + 4649145259
50  Browning-Financial.WealthBox-Addon  0x000000010edc5593 mono_jit_runtime_invoke + 1443 (mini-runtime.c:2806)
51  Browning-Financial.WealthBox-Addon  0x000000010eeaee4f mono_runtime_invoke_checked + 127 (object.c:2887)
52  Browning-Financial.WealthBox-Addon  0x000000010eeb5c2e mono_runtime_exec_main_checked + 110 (object.c:4782)
53  Browning-Financial.WealthBox-Addon  0x000000010ed1087f mono_jit_exec + 287 (driver.g.c:1210)
54  Browning-Financial.WealthBox-Addon  0x000000010ed1384a mono_main + 11114 (driver.g.c:2418)
55  Browning-Financial.WealthBox-Addon  0x000000010ecb0a8e xamarin_main + 1182 (launcher.m:661)
56  Browning-Financial.WealthBox-Addon  0x000000010ecb1a04 main + 36 (launcher.m:679)
57  libdyld.dylib                   0x00007fff7ef42ed9 start + 1

Thread 1:
0   libsystem_pthread.dylib         0x00007fff7f1353f8 start_wqthread + 0
1   ???                             0x0000000054485244 0 + 1414025796

Thread 2:: SGen worker
0   libsystem_kernel.dylib          0x00007fff7f07f7de __psynch_cvwait + 10
1   libsystem_pthread.dylib         0x00007fff7f139593 _pthread_cond_wait + 724
2   Browning-Financial.WealthBox-Addon  0x000000010ef7416e thread_func + 558 (mono-os-mutex.h:173)
3   libsystem_pthread.dylib         0x00007fff7f136305 _pthread_body + 126
4   libsystem_pthread.dylib         0x00007fff7f13926f _pthread_start + 70
5   libsystem_pthread.dylib         0x00007fff7f135415 thread_start + 13

Thread 3:: Finalizer
0   libsystem_kernel.dylib          0x00007fff7f07c1b6 semaphore_wait_trap + 10
1   Browning-Financial.WealthBox-Addon  0x000000010ee4c7a5 finalizer_thread + 293 (mono-os-semaphore.h:90)
2   Browning-Financial.WealthBox-Addon  0x000000010ef01b50 start_wrapper + 704 (threads.c:1004)
3   libsystem_pthread.dylib         0x00007fff7f136305 _pthread_body + 126
4   libsystem_pthread.dylib         0x00007fff7f13926f _pthread_start + 70
5   libsystem_pthread.dylib         0x00007fff7f135415 thread_start + 13

Thread 4:: Debugger agent
0   libsystem_kernel.dylib          0x00007fff7f07f3e6 __recvfrom + 10
1   Browning-Financial.WealthBox-Addon  0x000000010ed0515e socket_transport_recv + 78 (debugger-agent.c:1153)
2   Browning-Financial.WealthBox-Addon  0x000000010ecf5a7c debugger_thread + 28588 (debugger-agent.c:1558)
3   Browning-Financial.WealthBox-Addon  0x000000010ef01b50 start_wrapper + 704 (threads.c:1004)
4   libsystem_pthread.dylib         0x00007fff7f136305 _pthread_body + 126
5   libsystem_pthread.dylib         0x00007fff7f13926f _pthread_start + 70
6   libsystem_pthread.dylib         0x00007fff7f135415 thread_start + 13

Thread 5:
0   libsystem_pthread.dylib         0x00007fff7f1353f8 start_wqthread + 0
1   ???                             0x0000000054485244 0 + 1414025796

Thread 0 crashed with X86 Thread State (64-bit):
  rax: 0x0000000000000000  rbx: 0x000000011d9ae5c0  rcx: 0x00007ffee0f81a18  rdx: 0x0000000000000000
  rdi: 0x0000000000000307  rsi: 0x0000000000000006  rbp: 0x00007ffee0f81a50  rsp: 0x00007ffee0f81a18
   r8: 0x0000000000000000   r9: 0x0000000000989680  r10: 0x0000000000000000  r11: 0x0000000000000206
  r12: 0x0000000000000307  r13: 0x000000011597b000  r14: 0x0000000000000006  r15: 0x000000000000002d
  rip: 0x00007fff7f08223e  rfl: 0x0000000000000206  cr2: 0x00007fffb1d2c188

Logical CPU:     0
Error Code:      0x02000148
Trap Number:     133
kanaloa
  • 31
  • 8
  • Also, I notice that, in your setter, you are catching and silently swallowing exceptions. Are you sure that this isn't masking some sort of error? See [Why not catch general Exceptions](https://stackoverflow.com/q/1742940/3744182). – dbc Jan 10 '19 at 01:33
  • Hey dbc, I chose not to catch exceptions in the model because the xcode control calls change events on every keystroke. So when the user tries to update a date field using the databound Outline View there's an exception on every keystroke until the string has enough characters to convert the value to DateTime. Ideally I'd rather use an event that's called on end edit like good old .net DataGrid. But I haven't found the equivalent of that in my short time with xamarin.mac so far. – kanaloa Jan 10 '19 at 04:21
  • I also get the same problem when trying deserialze the much simpler SimpleContact class, so I don't think the silenced exceptions in the full class are affecting the deserialization. I'll edit the question and post the full exception. – kanaloa Jan 10 '19 at 04:25

2 Answers2

1

It is always better to stick with the plain DTO (Data Transfer Object) when serializing/deserializing data which are POD (Plain Old Data, i. e. contains only get/set properties of fields with no business logic associated).

In that sense your ContactRoot type is POD, however Contact type is not POD (due to inheritance of NSObject). For this scenario I would introduce a separate DTO type for Contact which would follow POD standard. You could create an alternative property for contacts list and have a play with JsonIgnore and JsonProperty attributes to mimic desiried behavior.

Quick snippet:

public class ContactDto
{
    /* List relevant properties here */
}

public class ContactRoot
{
    [JsonIgnore] // Do not serialize this
    public List<Contact> Contacts { get; set; }

    [JsonProperty(Propertyname = nameof(Contacts)]
    public List<ContactDto> SerializableContacts { 
        get => Contacts?.Select(x => /* data extraction */).ToList();
        set => Contacts = value?.Select(x => /* data extraction */)?.ToList();
    }
}

Addition: it appears that for CLR languages POD is called POCO

  • Thanks Evgeny! You inspired my solution. I've never seen get & set methods like yours, looks super cool. I couldn't figure out how to use those, how do they work? – kanaloa Jan 10 '19 at 07:15
1

OK, thanks to Evengy Nazarov who pointed me in the right direction, I have a working solution.

First, here's a POCO verson of my simplified class:

public class ContactDto
{
    private string _fullname = String.Empty;

    public int id { get; set; }

    public string first { get; set; }

    public string last { get; set; }

    [JsonIgnore]
    public string fullname {
        get { return this.first + " " + this.last; }
        set { _fullname = value; }
    }
}

Notice no NSObject inheritance or property decorations (other than [JsonIgnore].

Changed ContactRoot to this:

public class ContactRoot
{
    [JsonIgnore] // Do not serialize this
    public List<SimpleContact> SimpleContacts { get; set; }

    [JsonProperty(PropertyName = nameof(Contacts))]
    public List<ContactDto> SerializableContacts { get; set; }
}

Then I created conversion methods to convert between the DTO and the NSObject versions using System.Reflection:

public ContactDto ConvertToDto(SimpleContact simpleContact)
    {
        var contactdto = new ContactDto();
        foreach (PropertyInfo prop in simpleContact.GetType().GetProperties().Where(t => t.DeclaringType == typeof(SimpleContact)))
        {
            var value = prop.GetValue(simpleContact);
            if (value != null)
            {
                var prop2 = contactdto.GetType().GetProperty(prop.Name);
                prop2.SetValue(contactdto, value);
            }
        }
        return contactdto;
    }
public SimpleContact ConvertToContact(ContactDto contactDto)
    {
        var simplecontact = new SimpleContact();
        foreach (PropertyInfo prop in contactDto.GetType().GetProperties())
        {
            var value = prop.GetValue(contactDto);
            if (value != null)
            {
                var prop2 = simplecontact.GetType().GetProperty(prop.Name);
                prop2.SetValue(simplecontact, value);
            }
        }
        return simplecontact;
    }

Then my new Serialize method:

var contactroot = new ContactRoot
            {
                SimpleContacts = DataSource.SimpleContacts,
                SerializableContacts = new List<ContactDto>()
            };

            foreach (var simplecontact in contactroot.SimpleContacts)
            {
                contactroot.SerializableContacts.Add(ConvertToDto(simplecontact));
            }

            var desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
            var filename = Path.Combine(desktop, "WBAddon.json");
            if (File.Exists(filename))
            {
                File.Delete(filename);
            }

            using (StreamWriter file = File.CreateText(filename))
            {
                var serializer = new JsonSerializer()
                {
                    Formatting = Formatting.Indented,
                    TypeNameHandling = TypeNameHandling.Auto,
                    TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple,
                    ReferenceLoopHandling = ReferenceLoopHandling.Ignore
                };
                await Task.Run(() => serializer.Serialize(file, contactroot));
            }

And Deserialization:

var jsettings = new JsonSerializerSettings
            {
                Formatting = Formatting.Indented,
                TypeNameHandling = TypeNameHandling.Auto,
                TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple,
                ReferenceLoopHandling = ReferenceLoopHandling.Ignore
            };
            string json = string.Empty;
            using (StreamReader file = new StreamReader(filename))
            {
                json = file.ReadToEnd();

            }

            var contactroot = JsonConvert.DeserializeObject<ContactRoot>(json);
            DataSource.SimpleContacts = new List<SimpleContact>();
            foreach (var contactdto in contactroot.SerializableContacts)
            {
                DataSource.SimpleContacts.Add(ConvertToContact(contactdto));
            }

Works like I hoped! Now I just need to create a dto version of my full Contact class with all 70+ properties. There might be a better way but this gets the job done.

kanaloa
  • 31
  • 8