21

So let's say I've got a class that looks like this:

public class APIParams {
        public String name; 
        public String category; 
        public Boolean taxable = false;
}

Then I have a custom REST webservice (apexrest) that takes JSON and deserializes it to an object of this class, like so:

    String rawParams = RestContext.request.requestBody.toString();
    APIParams p = (APIParams) JSON.deserialize(rawParams, APIParams.class);

The difference in behavior comes in how it handles it if I don't pass in that "taxable" property. Here's what I'm seeing:

  • In a unit test, if I pass in JSON that doesn't include "taxable", I end up with an object where taxable = false. (i.e. it respects the default value in the class definition)
  • OTOH, if I pass in the same JSON via the API, I end up with an object where taxable = null. No default is applied.

Is there some sort of rhyme & reason to this difference, or is this effectively a bug? As you might imagine, this makes it difficult to write tests that effectively cover the behavior that we will see in production.

Thanks!

7
  • 2
    Can you call the web service in the development environment? It might be more of a unit testing versus actual web service issue rather than a production vs development issue. You could also try calling JSON.deserialize in anonymous Apex in production. Nov 19, 2014 at 23:31
  • 3
    Note that an apex rest service will do automatic deserialization if you make the custom type an argument to your post-annotated method. I tend to put getters on my Boolean properties in custom types that look like return taxable == null ? false : taxable;
    – cropredy
    Nov 19, 2014 at 23:43
  • @DanielBallinger I do believe this is a matter of unit test vs API, rather than what kind of org it is. I will update the title of the post. It's still a problem, though. ;)
    – mscholtz
    Nov 19, 2014 at 23:47
  • @crop1645 thanks for the tip on the automatic deserialization. I may try that at some point on the off chance that different syntax may lead to different behavior. And yes, I know there are lots of ways to work around this, but regardless of that, IMHO the test should work the same as the API call so that it's more likely I'll know that a workaround is needed.
    – mscholtz
    Nov 19, 2014 at 23:51
  • Well, Boolean variables in custom types or standalone can definitely have null values; they can't in sobjects. You should post your unit test code (stripped to its essence)
    – cropredy
    Nov 19, 2014 at 23:55

2 Answers 2

2

I tried a test as you mentioned but it has the same behavior on Test as well as on real usage.

@IsTest
public class APIParamsTest {
    @isTest static void test() {
        String rawParams = '{"name":"karthik"}';
        APIParams p = (APIParams) JSON.deserialize(rawParams, APIParams.class);
        System.assert(p.name == 'karthik');
        System.assert(p.taxable == false);
    }
}

Even in this test the assert on taxable field fails.

  • I am not sure how its different from the test you wrote, but I find the behavior consistent everywhere.

  • To get consistent behavior I think its better to handle it in getter than through default variables As @crop1645 mentioned.

    return taxable == null ? false : taxable;

0

karthikselva's answer is spot-on, that JSON.deserialize will produce the same result in your use case where taxable is null if the field is omitted in either a unit test or in a real API call.

This is not a bug but rather an intended feature of serialization and deserialization. Microsoft explains the concept of "serialization" rather well in its docs.

Serialization allows the developer to save the state of an object and recreate it as needed, providing storage of objects as well as data exchange.

The point of the serialized string is that it represents the exact state of an object, so that when you deserialize the string you get the object exactly as it was when it was serialized. If serialization were to trigger other code that may modify the values of member variables, the point of serialization would be lost.

Proposed solution

In your situation, you could explicitly create the behavior you want by using a getter method as follows, handling the situation where taxable is null.

public class APIParams {
    public String name; 
    public String category;

    /**
     * Note that this field is now private.
     */
    private Boolean taxable = false;

    public Boolean getTaxable() {
        return this.taxable == null ? false : this.taxable;
    }

    public void setTaxable(Boolean value) {
        this.taxable = value;
    }
}

A modified APIParams example to illustrate serialization behavior

For clarity, let's move the default value assignments into a constructor.

public class APIParams {
    public String category; 
    public String name; 
    public Boolean taxable;

    public APIParams() {
        this.name = 'UNKNOWN';
        this.category = 'UNKNOWN';
        this.taxable = false;
    }
}

When constructing new instances of the APIParams class in Apex, you would normally have to call the APIParams() constructor, which executes code that sets default values.

Now consider the unit test below.

@IsTest
private class APIParamsTest {

    @IsTest
    private static void deserializeNameOnly() {

        // Given
        String jsonBody = '{"name":"Acme"}';

        // When
        Test.startTest();

        APIParams params = (APIParams)JSON.deserialize(jsonBody, APIParams.class);

        // Then
        Test.stopTest();

        System.assertEquals('Acme', params.name, 'name');
        System.assertEquals(null, params.category, 'category');
        System.assertEquals(null, params.taxable, 'taxable');
    }
}

You will see that the constructor is never executed by design.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .