70

Is there any way to deserialize JSON into an object using JSON.deserialize if some of the property names in the JSON are reserved words in Apex? I want to do something like this:

string jsonString = '{"currency" : "ABC"}';
public class JSONResult
{
    public string currency;
}
JSONResult res = (JSONResult) JSON.deserialize(jsonString, JSONResult.class);
system.debug(res);

but of course I get an error that the identifier name is reserved.

fred
  • 3,335
  • 3
  • 16
  • 27
Greg Grinberg
  • 7,471
  • 1
  • 39
  • 71

5 Answers5

68

There are 2 ways that you could solve this problem, neither of them is exactly what you're looking for, but I think it's the best apex offers.

  1. Perform a string replace on the json string, the implications of this is unintended replacement of valid text inside a string, but this could be mitigated if the form of the json is as you've supplied "currency": "ABC" you could string replace:

    jsonString.replace('"currency":', '"currency_x":');

  2. The other is a lot more painful, it would require you to parse the json yourself using the JSON Parser methods. This will be a lot more accurate, but if the definition of the json changes you will have to rewrite your solution

Daniel Ballinger
  • 102,288
  • 39
  • 270
  • 594
Daniel Blackhall
  • 7,882
  • 10
  • 50
  • 68
  • The JSON Parser link is dead. The most recent archived version seems to be from 2012: https://web.archive.org/web/20121215114935/http://www.salesforce.com:80/us/developer/docs/apexcode/Content/apex_methods_system_jsonparser.htm – dbc Jul 21 '21 at 17:10
16

I've created a abstract class to allow for two-way serialization of JSON that has reserved keywords. There are definitely some limitations but it works for everything I've tried so far (google calendar & jira API).

/* Author: Charlie Jonas (charlie@callawaycloudconsulting.com)
 * Description: Allows reserved named serialization.
 * Usage:  See Readme @ https://github.com/ChuckJonas/APEX-JSONReservedNameSerializer
 */
public abstract class JSONReservedSerializer {
    private final Map<Type,Map<String,String>> typeMapKeys;

    public JSONReservedSerializer(Map<Type, Map<String, String>> typeMapKeys){
        this.typeMapKeys = typeMapKeys;
    }

    public String serialize(Object obj, System.Type type){
        return serialize(obj, false, type);
    }

    public String serialize(Object obj, Boolean suppressNulls, System.Type type){
        String retString = JSON.serialize(obj, suppressNulls);
        retString = transformStringForSerilization(retString, typeMapKeys.get(type));
        return retString;
    }

    public Object deserialize(String jsonString, System.Type type){
        jsonString = transformStringForDeserilization(jsonString, typeMapKeys.get(type));
        return JSON.deserialize(jsonString, type);
    }

    private static String transformStringForSerilization(String s, Map<String, String> mapKeys){
        return replaceAll(s, mapKeys);
    }

    private static String transformStringForDeserilization(String s, Map<String, String> mapKeys){
        Map<String,String> flippedMap = new Map<String,String>();
        for(String key : mapKeys.keySet()){
            flippedMap.put(mapKeys.get(key), key);
        }
        return replaceAll(s, flippedMap);
    }

    private static String replaceAll(String s, Map<String,String> toFromMap){
        for(String key : toFromMap.keySet()){
            s = s.replaceAll('"'+key+'"(\\ )*:', '"'+toFromMap.get(key)+'":');
        }
        return s;
    }
}

Implementation looks like this:

public class MySerializer extends JSONImprovedSerializer {

  private MySerializer() {
    //setup mappings
    super(new Map<Type,Map<String,String>>{
      MyOuterDTO.class => OUTER_DTO_MAPPINGS
    });
  }

  //define DTO's using mapped names
  static final Map<String, String> OUTER_DTO_MAPPINGS = new Map<String, String> {
      'obj' => 'object',
      'isPrivate' => 'private'
  };

  public class OuterDTO {
    public InnerDTO obj;
  }

  public class InnerDTO {
    public Boolean isPrivate;
    public String notReserved;
  }
}

Usage (round trip serialization):

String origString = '{"object":{"private":true,"notReserved":"abc"}}';

//deserialization
MySerializer json = new MySerializer();
MySerializer.OuterDTO dto = (MySerializer.OuterDTO) json.deserialize(
  origString,
  MySerializer.OuterDTO.class
);

//serialization
String newString = json.serialize(obj);
System.assertEquals(origString, newString);

UPDATE

I Created a to a repo with install instruction using the sfdx-cli

NSjonas
  • 10,145
  • 9
  • 64
  • 105
  • String.replace() is much shorter and much more performant – Alex Kalatski Jun 02 '23 at 14:58
  • @AlexKalatski sure is! Seems like a general statement you could apply to any class.

    And did you test how "much more performant" it actually is? Pretty sure when the essential operations are string.replace & json.seralize in both solutions, some class initialization probably is pretty insignificant.

    – NSjonas Jun 02 '23 at 20:19
6

To add to the 2 ways in the accepted answer, here is a third way making use of Apex maps that allow any string as the key:

Map<String, Object> m = (Map<String, Object>)JSON.deserializeUntyped('{"QPA$MV2": "xyz"}');
String value = (String) m.get('QPA$MV2');
System.assertEquals('xyz', value);

This approach also works when you want to serialize to JSON.

Keith C
  • 135,775
  • 26
  • 201
  • 437
6

Can suffix the JSON programmatically and deserialize it into a generated class:

public class DTO {
    String toString_x;
    String object_x;
    String class_x;
    String new_x;
}

For example:

String data = '{"class": ""}'; //bad words etc...
Object input = Json.deserializeUntyped(data);

String suffixed = new ReservedWordSerializer(obj).getAsString(); DTO dto = (DTO)Json.deserialize(suffixed, DTO.class);

//no more bad words System.debug(dto.class_x);

using the below generator to perform the suffixing:

/**
 * Usage:
 * new ReservedWordSerializer(obj).getAsString();
 */
public class ReservedWordSerializer
{
// true for pretty printing
JsonGenerator g = Json.createGenerator(true);

public ReservedWordSerializer(Object obj)
{
    if (obj == null)
    {
        g.writeNull();
    }
    else if (obj instanceof Map&lt;String,Object&gt;)
    {
        traverseMap((Map&lt;String,Object&gt;)obj);
    }
    else if (obj instanceof List&lt;Object&gt;)
    {
        traverseList((List&lt;Object&gt;)obj);
    }
    else
    {
        g.writeObject(obj);
    }
}

public String getAsString()
{
    return g.getAsString();
}

void traverseMap(Map&lt;String,Object&gt; obj)
{
    List&lt;String&gt; keys = new List&lt;String&gt;(obj.keySet());
    keys.sort();

    g.writeStartObject();
    for (String key : keys)
    {
        Object value = obj.get(key);
        g.writeFieldName(key + '_x'); // &lt;------ reserved word safety here

        if (value == null)
        {
            g.writeNull();
        }
        else if (value instanceof Map&lt;String,Object&gt;)
        {
            traverseMap((Map&lt;String,Object&gt;)value);
        }
        else if (value instanceof List&lt;Object&gt;)
        {
            traverseList((List&lt;Object&gt;)value);
        }
        else
        {
            g.writeObject(value);
        }
    }
    g.writeEndObject();
}

void traverseList(List&lt;Object&gt; objs)
{
    g.writeStartArray();
    for (Object obj : objs)
    {
        if (obj == null)
        {
            g.writeNull();
        }
        else if (obj instanceof Map&lt;String,Object&gt;)
        {
            traverseMap((Map&lt;String,Object&gt;)obj);
        }
        else if (obj instanceof List&lt;Object&gt;)
        {
            traverseList((List&lt;Object&gt;)obj);
        }
        else
        {
            g.writeObject(obj);
        }
    }
    g.writeEndArray();
}

}

Matt and Neil
  • 32,894
  • 7
  • 105
  • 186
3

We have to change the Key of JSON for sure .Reserved words cant be keys .We will run into compile time errors

Mohith Shrivastava
  • 91,131
  • 18
  • 158
  • 209