JSON array type resolution in ASP.Net webservices
Asked Answered
G

5

9

So I have worked out how to pass my custom objects into ASP.Net json webservices. Works a charm.

Problem I am having is passing in straight arrays of my custom objects or alternatively passing in arrays that are parameters of my custom objects.

So for example...

Public Class WebService1
    Inherits System.Web.Services.WebService

    <WebMethod()> _
    <ScriptMethod(ResponseFormat:=ResponseFormat.Json)> _
    Public Function AddPersonList(ByVal PersonList As PersonList) As String
        Debug.Assert(False)
    End Function

    Public Class Person
        Public Sub New()
        End Sub

        Public Property FirstName As String
        Public Property LastName As String
    End Class

    Public Class PersonList
        Inherits List(Of Person)

    End Class
End Class

<script>
        $(function () {
            $.ajax({
                type: "POST",
                url: "WebService1.asmx/AddPersonList",
                data: " { PersonList: [ { FirstName: 'Max', LastName: 'Gershkovich' }, { FirstName: 'Test1', LastName: 'Test2' } ] }",
                contentType: "application/json; charset=utf-8",
                dataType: "json",
                success: function (e) { debugger; },
                error: function (e) { debugger; }
            });
        });
    </script>

Fails with error: The value \"System.Collections.Generic.Dictionary`2[System.String,System.Object]\" is not of type \"WebApplication1.WebService1+Person\" and cannot be used in this generic collection.\r\nParameter name: value

So how do I explain to ASP.net that it is infact an array of Person?

Please note: That changing the function to as List(of Person) or ArrayList does work but given that I implement my own custom collections this is not optimal for me.

UPDATE: Ok so what I have worked out so far is that this problem is definitely associated with how the JavascriptSerializer uses the SimpleTypeResolver to resolve types. Basically if I do something like this

Public Function AddPersonList(ByVal PersonList As String) As PersonList

I can recreate the error using the following code.

Dim PersonList As PersonList = jsonSerializer.Deserialize(Of PersonList)(PList)

However when I provide my own custom type resolver along the lines of

Dim jsonSerializer As New JavaScriptSerializer(New MyCustomTypeResolver)

I can successfully create an instance of my custom list.

Now I have worked out how to provide my own custom convertor in the web.config file. Along the lines of this….

<system.web.extensions>
    <scripting>
      <webServices>
        <jsonSerialization>
          <converters>
            <add name="MyCustomConverter" type="WebApplication1.MyCustomConverter" />
          </converters>
        </jsonSerialization>
      </webServices>      
    </scripting>
  </system.web.extensions>

But the problem is that I can’t find a way to specify a CustomTypeResolver. My assumption is that this is the only piece of the puzzle I am missing.

PSS: I have tried to use the assembly fully qualified name as specified in the docs for the SimpleTypeResolver (SimpleTypeResolver MSDN) but this throws a "Operation is not valid due to the current state of the object." exception - which is an error caused when the TypeResolver cannot resolve the name (I know this by testing with my CustomTypeResolver)

UPDATE @ Oleg and Sean:

I sincerely appreciate your input and have infact changed my application to reflect your suggestions; having said that the problem with this change is that it requires reworking of my class implementation and its associated behaviour in ways that I would have preferred to avoid.

My problem was never passing in a List(of Object) into my webservice (I simply posed the question as such to simplify it for stackoverflow). In such a case I would be willing to completely accept using a Generic List(of) but my problem was actually that one of my custom objects implemented a property with a strong typed List(of) so for example:

Customer {CustomerID AS Guid, FirstName AS String, LastName AS String, EmailAddresses AS EmailAddressList}

which now needs to be changed to

Customer {CustomerID AS Guid, FirstName AS String, LastName AS String, EmailAddresses AS List(Of EmailAddress)} 

This is admittedly not the end of the world and is probably better in the webservice context (as you have suggested) but definitely a detriment when it comes to internal application usage of my Collection properties. What this means is that once I have this property I either need to cast it to my CustomCollectionList every time I want to use some advanced feature or I need to implement another property which exposes the CustomCollectionList. Either solution leaves a nasty taste in my mouth.

Grower answered 6/1, 2011 at 5:12 Comment(1)
As far as I am concerned this is a limitation of the serialization mechanism in .NET for JSON. I am still yet to see an example of this functionality and haven't been able to work it out myself. Just reread the question after an upvoteGrower
G
3

First of all the data

{ PersonList: [ { FirstName: 'Max', LastName: 'Gershkovich' }, { FirstName: 'Test1', LastName: 'Test2' } ] }

which you send to the web service are wrong JSON data. You can verify the data on http://www.jsonlint.com/. Correct JSON data will be

{"PersonList":[{"FirstName":"Max","LastName":"Gershkovich"},{"FirstName":"Test1","LastName":"Test2"}]}

Moreover I recommend you never make JSON manually. Instead of that you should use JSON.stringify. In the most cases the function are even natively supported in the web browsers (sometime after updates like here). Correct serialization of Web service parameters can be

$.ajax({
    data: {PersonList:JSON.stringify(t)} 
    // other parameters
});

(see here for more details) or

$.ajax({
    data: JSON.stringify({ PersonList: t })
    // other parameters
});

where

var t = [ { FirstName: 'Max', LastName: 'Gershkovich' },
          { FirstName: 'Test1', LastName: 'Test2' } ];

Which version of data representation JSON.stringify({ PersonList: t }) or {PersonList:JSON.stringify(t)} is correct depend on other things. It is easy to test which from there work in your environment.

Next small problem: you should better use List(Of Person) directly in the parameters of AddPersonList instead of the usage of types PersonList inherited from List(Of Person).

UPDATED: Just now I read your comments about List(Of Person) or PersonList to another answer. To prevent the same discussion I decide to write my opinion about this. It is not important which classes you use inside of your web service. You should design the interface of the web service so that it should be simple and clear for every person who knows nothing about your implementation. The input data of the method (at least what you included in the question) can be perfectly described with List(Of Person). Moreover List(Of Person) it is good for the data transfer. Inside of the method implementation you can convert List(Of Person) to any other classes which are good for the method internals. For example, it is very simple to construct PersonList from the List(Of Person). So I recommend you to follow the way: keep interface of the web service free from the implementation details.

UPDATED 2: It is not a problem at all if the object, which is the input parameter of the web method, has as the property another objects like List(Of EmailAddress). For example, if you defines EmailAddress from your last example as

Public Class EmailAddress
    Public Property Type As String
    Public Property Address As String
End Class

and Customer like

Public Class Customer
    'Public CustomerID As Guid
    Public Property FirstName As String
    Public Property LastName As String
    Public Property EmailAddresses As List(Of EmailAddress)
End Class

then you will have no problem with AddPersonList web method defined as following

<WebMethod()> _
Public Function AddPersonList(ByVal PersonList As List(Of Customer)) As String

In the same way you can use

<WebMethod()> _
Public Function AddPersonList1(ByVal input As InputData) As String

with

Public Class InputData
    Public Property PersonList As List(Of Customer)
End Class

or many other versions of object to send the information to the web server using $.ajax.

On the client side you can define myData as following

var myData = [
        { FirstName: 'Max', LastName: 'Gershkovich',
            EmailAddresses: [
                { Type: 'SMTP', Address: '[email protected]' },
                { Type: 'SMTP', Address: '[email protected]' },
            ]
        },
        { FirstName: 'Test1', LastName: 'Test2' }
    ];

and then send the data to the web server with

$.ajax({
    type: "POST",
    url: "WebService1.asmx/AddPersonList",
    data: JSON.stringify({ PersonList: myData }),
    contentType: "application/json; charset=utf-8",
    dataType: "json",
    success: function (data) {
        alert(data.d);
    },
    error: function (xhr, textStatus, errorThrown) {
        alert("Error Occured!" + " | " + xhr.responseText + " | " +
              textStatus + " | " + errorThrown);
    }
});

or

$.ajax({
    type: "POST",
    url: "WebService1.asmx/AddPersonList1",
    data: JSON.stringify({ input: {PersonList: myData} }),
    contentType: "application/json; charset=utf-8",
    dataType: "json",
    success: function (data) {
        alert(data.d);
    },
    error: function (xhr, textStatus, errorThrown) {
        alert("Error Occured!" + " | " + xhr.responseText + " | " +
              textStatus + " | " + errorThrown);
    }
});

All the above work without any problem. You can download the test example, which do this (Default.htm should be started. To call the method you should click on the first or the second button).

If you look at the myData object defined in the client code and then look at the usage of JSON.stringify above you will see that you will have no problem in sending any complex objects including arrays and properties. On the server side you can use List(Of Customer), List(Of EmailAddress) or list of other objects as the representations of JavaScript Arrays. All will work. Only your original example with the object inheritance is a bad one.

If you will try to design the interface of the web service looking from the client side and not from the internal server structures you will easy construct the classes for the corresponding input parameters of the web methods. And all will work immediately. If you will need to initialize your internal classes with the information you will be able to do it very simply. Such kind of data conversion will be very easy and you will not need to write any CustomTypeResolver which do in fact the same.

Grouse answered 8/1, 2011 at 12:57 Comment(0)
N
1

UPDATE: The first step would be to move from .asmx to WCF's .svc - it's way more flexible and its serializer is much smarter.

I normally just use a generic list argument, this has not failed me so far.

If you need to use a custom ObjectList property instead of a generic list<Object>, how about hiding the ObjectList property from the serialization and have a list<Object> property proxy between the two?

That way the serializer could serialize and deserialize the list<Object> property, while you could continue working with your ObjectList property.

This is easily done in WCF by using the DataMember attribute, and by using DataMember(Name='ObjectList') you can even continue using the exact same property names in js.

All in all you get a property that behaves as an array in javascript, and as a ObjectList in .net.

Nelia answered 9/1, 2011 at 23:41 Comment(1)
Sorry just reread your idea (I know bad after all this time) but its actually not a bad one either. Could mark the property NotSerializable (or whatever that attribute is).Grower
K
0

Your data seems not be in line with JSON format. Please try using

data: " { PersonList: [ { 'FirstName': 'Max', 'LastName': 'Gershkovich' }, { 'FirstName': 'Test1', 'LastName': 'Test2' } ] }"

To be at a safer side, use the json library to convert data to json.

Kenney answered 6/1, 2011 at 5:26 Comment(5)
Tried it but same problem. It appears to parse the JSON without a problem. Judging by the error message this seems to me like some sort of Type resolution error just not sure how to map the array to my list object correctly.Grower
Tried and works fine for me. Did you include the <ScriptService> attribute on webservice class?Kenney
Yes I did, did you use the exact same code? My guess is you used something like Public Function AddPersonList(ByVal PersonList As ArrayList) As String which also works fine for me???Grower
Yes. I used List(Of Person) instead of PersonList. Is there a specific reason for using class inheriting from List(Of Person)..?Kenney
Yeah there is, I implement my own custom collection classes. They are all List(Of) but contain custom logic for various uses. (Generally for database access) So cant walk away from them PS: Your help is much appreciated.Grower
U
0

Just a guess, is there a leading space in your data string? Between " and {.

data: " { PersonList: [ { FirstName: 'Max', LastName: 'Gershkovich' }, { FirstName: 'Test1', LastName: 'Test2' } ] }",                 
       ^

Try taking the space out. Any change?

Unsociable answered 6/1, 2011 at 5:54 Comment(2)
There was.... Tried, still no good :-( same error. It seems to me like I am going to need to register a custom convertor and do something along those lines.Grower
Space will not be an issue. Check my comment for possible solutionKenney
M
0

While Oleg's answer seems rather comprehensive and the best so far, I wanted to provide an alternate approach and recommend the .NET NewtonSoft JSON Library.

I agree that it is good to have your service methods provide a clear definition of the parameters, however, when I've run into problems and needed to get something working asap, accepting a string and parsing out with this library has proven useful in pinch.

Making answered 13/1, 2011 at 22:54 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.