locked
Serialization Loss of JSONProperty Name annotations when returning from Web API RRS feed

  • Question

  • User301720682 posted

    Hello, I have the following situation

    • Web API Service running on Azure, with several web apis that return back an instance of an object
    • Shared data model project inside the same solution as the API Service
    • An MVC Web Client that runs on Azure as well and uses the same Shared data model project as the API Service

    • The MVC Client has views that let you input data
    • The MVC Client Controller/Action then sends the request to the Web API Service and pumps the output to another view
    • The Service gets the request and calls another 3rd party service to get a response to send back to the Web Client

    • The Service gets the response and attempts to serialize it into a .NET Class in the shared data model 
    • The .NET class did NOT work initially because the incoming JSON formatted names were not the same as what was in the .NET Class. So I added JSONProperty Name Annotations and it works perfectly

    • However, when the Web Client tries to serialize the response JSON from the Service into the same .NET Class it fails (just leaves it null). Remembering that this Class is the same class and coming from the same DLL / Project as they are all in the same solution as the Service uses

    What I tried

    1. In the Service, right after de-serializing the 3rd party data into my .NET class, I re-serialize it to a JSON string with JSONConvert and it DOES in fact have the property JSONProperty annotated names (good). So I do see serialization is using the JSON names as expected

    2. However, its still wrong on the client side.
    3. On the client side, I created a new class, just for testing, that was just a copy and pass from my data models .NET class and then removed the JSONProperty Name attributes.
    4. Immediately the Client property de-serialized the content into the "new" class instance

    But this is not what I want. I don't want to have to have 2 sets of the exact same classes (as we have allot).

    Is there any reason that the JSON.NET serializer (assuming that is the default for Web API now????) would ignore the JSONProperty attributes, when returning a response on a Web API?

    Example

    public class foobar {

         private string flopper;

         [JsonProperty("flopsee")]

         public string Flopper (getter/setter)

    }

    The above works great when I first get the response from the 3rd Party into my Service. Or if I immediately re-serialize the newly created de-serialized class instance, after getting the 3rd party response.

         foobar myfoobar = get response in json from 3rd party

    it works great

         string jsonstring = JsonConvert.Serialize(myfoobar)

    I can see the names in the string properly

         foobar myfoobar2 = JsonConvert.Deserialize<foobar>(jsonstring)

    again works great

    But on the client 

         foobar myfoobarclient = get response in json from web service

    Even though the JSON response is there and I can see, and see it has NO annotated names just regular . NET Names coming in

         myfoobarclient == null

    So I created a new local version of the class without the JSONProperty name annotation, since I didn't see it come across the Wire this way

    public class foobarlocal {

         private string flopper;

         //removed JSONProperty

         public string Flopper (getter/setter)

    }

    Now this works

         foobarlocal myfoobarclient = get response in json from web service

    But I wanted foobar = myfoobar to work... not have to create another class

    Thanks,

    Monday, April 20, 2020 5:22 PM

Answers

  • User665608656 posted

    Hi,

    You should not use the "ReadAsStringAsync()" method. You can try to use "ReadAsAsync<RepoDataMaster>()" to get the object, and then serialize and deserialize, so that it can meet your needs.

    More details, you could refer to below code:

    RepoDataMaster repoDataMaster = await response.Content.ReadAsAsync<RepoDataMaster>();
    string validatingJsonHasJSONPropertyNames = JsonConvert.SerializeObject(repoDataMaster);
    RepoDataMaster masterdata = JsonConvert.DeserializeObject<RepoDataMaster>(validatingJsonHasJSONPropertyNames);

    Best Regards,

    YongQing.

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Wednesday, April 22, 2020 7:01 AM

All replies

  • User475983607 posted

    As far as I can tell, the client and service class property names do not match.  The service serializes to flopsee while the client serializes to flopper.

    Service

        public class foobar
        {
            [JsonProperty("flopsee")]
            public string Flopper { get; set; }
        }

    Client

        public class foobarlocal
        {
            public string Flopper { get; set; }
        }

    The simplest solution is sharing the same POCO or specifically mapping the 3rd party type to your POCO.  Then use the POCO in your application. 

    Monday, April 20, 2020 7:43 PM
  • User301720682 posted

    HI mgebhard

    Thanks for the reply.

    They do match, what I said was that I was originally use the exact same class, same shared data models project, so both the Service and the Client were using

        public class foobar { [JsonProperty("flopsee")] public string Flopper { get; set; } }

    But that I then created a local different version, without the JsonProperty naming annotation, which then allowed it to work.

    The 3rd party calls it flopsee.

    .NET calls it Flopper

    Serialization should call it flopsee, it doesn't, not when it passed it from Service to Client.

    Monday, April 20, 2020 8:17 PM
  • User-474980206 posted

    if the internal json names don't match, the external json names, then you need two models or a custom serializer.

    Monday, April 20, 2020 8:21 PM
  • User301720682 posted

    hi Bruce

    Thanks!!

    But what I don't get it why they don't match is what I don't get...

    The whole point of adding a JSONProperty("new name") is so that on incoming it knows what .NET member to populate with that data AND what to call it on the way out.

    incoming JSON

    { "flopsee" : "me" }

    becomes

    foobar.Flopper because of the JSONProperty("flopsee"). Annotation.

    When you immediately re-serialize foobar into a json string, you SEE { "flopsee" : "me" } which you should because you have the annotation

    But when the Service returns this object foobar to the Client, the client sees "Flooper"... My question is WHY? The class that was being used (and the only one being used) was Foobar. and Foobar said JSONProperty("floopsee").

    I only added the second class, to see if removing the annotation would work.

    I thought Annotations were not just used on Incoming but also in Serialization.

    Monday, April 20, 2020 8:26 PM
  • User475983607 posted

    I cannot reproduce this issue.  

        public class foobar {
            [JsonProperty("flopsee")] 
            public string Flopper { get; set; } 
        }
    
        public class ValuesController : ApiController
        {
    
            public foobar Get()
            {
                return new foobar { Flopper = "Hello World" };
            }

    Results

    {
        "flopsee": "Hello World"
    }

    Still your two classes have property names that do not match.   I assume that's the problem.  Or you have a bug in the code that we cannot see on the forum.

    Monday, April 20, 2020 8:48 PM
  • User301720682 posted

    Hello again,

    I created a repro and per say I think I see what is happening, I just don't know how to fix it as its not in my code doing in.

    I created a new set of classes in my Shared Data Model project that both Client and Service use.

    [Serializable]

    public class DataFromExternal

    {

    #region private properties

    private string dataContext;

    private TheData[] theData;

    private string oDataNextPage;

    #endregion

    #region public properties

    [JsonProperty("@odata.context")]

    public string DataContext { get => dataContext; set => dataContext = value; }

    [JsonProperty("value")]

    public TheData[] TheData { get => theData; set => theData = value; }

    [JsonProperty("@odata.nextLink")]

    public string ODataNextPage { get => oDataNextPage; set => oDataNextPage = value; }

    #endregion

    }

    I have a secondary class to support the value property below

    [Serializable]

    public class TheData

    {

    #region private Properties

    private string id;

    private string createdDateTime;

    #endregion

    #region public properties

    [JsonProperty("id")]

    public string Id { get => id; set => id = value; }

    [JsonProperty("createdDateTime")]

    public string CreatedDateTime { get => createdDateTime; set => createdDateTime = value; }

    #endregion

    }

    In my Web API Service I created a Web API below. You will notice that there is a specific line, that re-serializes the result from the 3rd party just to see if the serializations matches what I need. It does.

    public async Task<DataFromExternal> GetDataFrom3rdparty([FromBody] WebClientRequest webClientRequest)

    {

        try

           {

          string AccessToken = await GetToken();

           Client client = new Client();

           DataFromExternal thedata = await client.GetDataFrom3rdparty(AccessToken, webClientRequest.Version, webClientRequest.AccessType, null);

         // The next line of code was JUST to see if I re-serialized it would it contain the @data.context etc as names in the JSON and it does.

          string thedatajson = JsonConvert.SerializeObject(thedata);

          return thedata; // return the object to the client

        }

          catch (Exception ex)

         {

          throw ex;

           }

    }

    Now in my Web Client

    public async Task<ActionResult> GetDataFrom3rdparty(WebClientRequest webClientRequest)

    {

       try

      {

        DataFromExternal thedata = null;

        AuthenticationResult result = await GetToken();

        if (result != null)

        {

    // This JSON does NOT have @data.context, it has dataContext, which is the "private" named version of my .Net class.., so it renamed it when it was passing it across the web.. but didn't rename it locally when I did this in the Service

          string json = await ExecutePlannerServiceCall(result.AccessToken, "GetDataFrom3rdparty", webClientRequest);

          if (!string.IsNullOrEmpty(json))

          {

               thedata = JsonConvert.DeserializeObject<DataFromExternal>(json);

               ViewBag.thedata = thedata;

          }

          else

          {

             ViewBag.grothedataups = null;

           }

        }

        return PartialView(thedata);

        }

        catch (Exception ex)

        {

           ViewBag.Message = ex;

           return View("Error");

        }

    }

    So I still don't understand, why Locally in the Server Service, I could serialize the object and its names were @data.context, but what gets passed across the wire is dataContext.

    Why does it lose the JSONProperty name?

    Monday, April 20, 2020 9:49 PM
  • User665608656 posted

    Hi, 

    Based on your code, first, you need to compare the json you request from webapi with locally in the Server Service requested json to see if there is a difference between them.

    Second, you need to check whether the version of "Newtonsoft.Json;" referenced in your client and model is the same.

    Best Regards,

    YongQing.

    Tuesday, April 21, 2020 9:34 AM
  • User301720682 posted

    Thanks Yongqing

    I already compare them, as per the notes in the code. The JSON coming from the 3rd party has their names in it. In the Service, if you use it to create an instance of the .NET class it works. If you immediately take that .NET class instance and turn it back into JSON, it has the 3rd party, JSONProperty annotated names in it. Which is a line of code above to demonstrate that.

    In the Client first thing I do is look at the JSON, and it does not have the 3rd party names in it, it has the .NET names in it, but since the class is expecting the names to match the 3rd party (a.k.a JSONProperty names), it will not deserialize and fails.

    All projects (Server, Models, Client) are in the same solution. All have the same JSON version. All are running on the same dev machine at the same time. Although I can repro this on any machine and have done so on several.

    Cheers,

    Tuesday, April 21, 2020 1:35 PM
  • User475983607 posted

    You have to understand that the community cannot reproduce this issue.  I have built and support many application that make calls to remote services from API, WCF, ASMX, etc.  I don't have this problem.  Most likely, you have a design bug.  That's why deploying the same code causes the same results.

    From my experience, it is uncommon for a remote server to use the same property names that my application uses and the shape of the data is usually different.  I like to build a mapping layer that takes the remote service response and uses it to populate my POCO.  The POCO I designed for my application that has all the names I expect.  Same idea for sending data to the remote service.  I'll take my POCO and convert it to the format the remote service expects.  Being that your remote service appears to be an OData service, mapping is probably the best option.

    Tuesday, April 21, 2020 1:48 PM
  • User301720682 posted

    Here is a full repro, would love if you will take this and try it.

    Load it, (one zip has the packages, one does not). Run and step through. Super simple easy repro.

    Link to the Project Zips

    • This is a single Project that is MVC/Web API so the Client and Server is in the same project
    • The datamodel project is merely there to "emulate" what my real stuff is, but is just a simple data model that has JSONProperty name @ names.
    • Set the RepoAllInOne as your start up.

    Put a break point in public async Task<ActionResult> RepoOfSerializationIssue()

    The first thing it does is creates an instance of the class and serializes it in the Web Client Controller. You can see the JSONProperty names in the JSON it creates.

    Then it calls the Server, and inside the Server controller it fakes an Asyn call, takes a JSON that has the @names in it, and serializes it into a Class instance.

    Then it de-serializes it, just like I did in the Client as a test, and you can see it has the @ names.

    Then it passed back the Object Class

    in the Client, you can see that the returned JSON does NOT have the @ names in it, which it should.

    So something happens in the serialization/deserialization from the call to the Server to the Response.

    Thanks,

    Tuesday, April 21, 2020 4:17 PM
  • User665608656 posted

    Hi,

    You should not use the "ReadAsStringAsync()" method. You can try to use "ReadAsAsync<RepoDataMaster>()" to get the object, and then serialize and deserialize, so that it can meet your needs.

    More details, you could refer to below code:

    RepoDataMaster repoDataMaster = await response.Content.ReadAsAsync<RepoDataMaster>();
    string validatingJsonHasJSONPropertyNames = JsonConvert.SerializeObject(repoDataMaster);
    RepoDataMaster masterdata = JsonConvert.DeserializeObject<RepoDataMaster>(validatingJsonHasJSONPropertyNames);

    Best Regards,

    YongQing.

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Wednesday, April 22, 2020 7:01 AM
  • User301720682 posted

    Thank you!!!!!!!

    Wednesday, April 22, 2020 1:13 PM