3

I'm writing an Apex REST API to match a public API standard (so I have no choice in names, here) that uses the JSON+HAL format. JSON+HAL APIs have result members that start with "_"(e.g. {"_links": [{name:"google",href: "http://google.com"}]}). As Apex doesn't allow member variables to start with "_", I need to use System.JSONGenerator. This is not so much of a pain.

However, I need to serialize arbitrary nested data structures and the inner data structures may include "_" too. Indeed, in every response there's a key _embedded and each embedded object has a _links key. So, each embeddable data type has to define it's own JSONGenerator to handle that _links key!

But, there doesn't seem to be a way to nest JSONGenerators, nor does there seem to be the ability to include a JSON string into a generator.

The data structure I'm working with is a bit heavy, but here's the full class to explain what's happening

@restResource(urlmapping='/hal')
global class HAL {
    @httpGet
    global static void getPeople() {
        HALResponse resp = new HALResponse();
        List<Contact> contacts = [SELECT Id,FirstName,LastName FROM Contact];
        resp.records = contacts.size();
        Link home = new Link();
        home.href='http://salesforce.com';
        resp.links.put('home',home);
        resp.embedded.put('people',peopleFromContacts(contacts));
        RestContext.response.addHeader('Content-Type', 'application/json');
        RestContext.response.responseBody = Blob.valueOf(resp.serialize());
    }

    global class HALResponse {
        global integer records {get;set;}
        global Map<String,Link> links {get;set;}
        global Map<String,List<Embeddable>> embedded {get;set;}
        global HALResponse() {
          this.links = new Map<String,Link>();
          this.embedded = new Map<String,List<Embeddable>>();
        }
        global String serialize() {
            JSONGenerator gen = JSON.createGenerator(true);
            gen.writeStartObject(); 
            gen.writeNumberField('records', records);
            if(links.size() !=0 ){
                gen.writeFieldName('_links');
                gen.writeStartObject();
                for(String linkKey : links.keySet()) {
                    gen.writeObjectField(linkKey,links.get(linkKey));
                }
                gen.writeEndObject();
            }
            gen.writeFieldName('_embedded');
            gen.writeStartObject();
            for(String embedType : embedded.keySet()) {
                gen.writeFieldName(embedType);
                gen.writeStartArray();
                for(Embeddable embed : embedded.get(embedType)) {
                    gen.writeString(embed.serialize());
                }
                gen.writeEndArray();
            }
            gen.writeEndObject();
            gen.writeEndObject();
            return gen.getAsString();
        }
    }

    global class Link {
        global string href {get;set;}
        global string title {get;set;}
        global String serialize() {
            JSONGenerator gen = JSON.createGenerator(true);
            gen.writeStartObject(); 
            if(String.isNotBlank(href)) {
                gen.writeStringField('href', href);
            }
            if(String.isNotBlank(title)) {
                gen.writeStringField('title',title);
            }
            gen.writeEndObject();
            return gen.getAsString();
        }
    }

    global class Person implements Embeddable {
        global Map<String,Link> links {get;set;}
        global string given_name {get;set;}
        global string family_name {get;set;}

        global Person() {
            this.links = new Map<String,Link>();
        }

        global String serialize() {
            JSONGenerator gen = JSON.createGenerator(true);
            gen.writeStartObject(); 
            if(String.isNotBlank(family_name)) {
                gen.writeStringField('family_name', family_name);
            }
            if(String.isNotBlank(given_name)) {
                gen.writeStringField('given_name',given_name);
            }
            if(links.size() !=0 ){
                gen.writeFieldName('_links');
                gen.writeStartObject();
                for(String linkKey : links.keySet()) {
                    gen.writeObjectField(linkKey,links.get(linkKey));
                }
                gen.writeEndObject();
            }
            gen.writeEndObject();
            return gen.getAsString();
        }
    }

    global static List<Person> peopleFromContacts(List<Contact> contacts) {
        List<Person> result = new List<Person>();
        for(Contact c:contacts) {
            Person p = new Person();
            p.given_name = c.FirstName;
            p.family_name = c.LastName;
            Link self = new Link();
            self.href='/'+c.Id;
            p.links.put('self',self);
            result.add(p);
        }
        return result;
    }

    global interface Embeddable {
        String serialize();
    }
}

The line that isn't working is gen.writeString(embed.serialize());. Instead of having the results of my serialize() method being included as normal JSON, they're being included as heavily quoted strings.

In short: I need a way to embed the results of one JSONGenerator into another.

2
  • 1
    why not use normal apex classes and normal apex variable names , serialize from that, and then do replaceAll() on the resulting string to get the _.... names you need before you submit the JSON?
    – cropredy
    Jan 5, 2016 at 4:52
  • @crop1645 - that's admittedly my backup plan. The deeply nested types could conceivably contain valid members named "links" or "embedded", and a replaceAll would blow those out of the water. But...those deeply nested types are currently defined without members that overlap with the _ members so I SHOULD be ok. Jan 5, 2016 at 4:58

1 Answer 1

4

Instead of trying to pass the String results from the JSONGenerator objects around, you can instead just pass a single JSONGenerator around.

The interface can be defined as

global interface Embeddable {
    JSONGenerator serialize(JSONGenerator generator);
}

The inner serialize methods look like this

    global JSONGenerator serialize(JSONGenerator gen) {
        gen.writeStartObject(); 
        if(String.isNotBlank(href)) {
            gen.writeStringField('href', href);
        }
        if(String.isNotBlank(title)) {
            gen.writeStringField('title',title);
        }
        gen.writeEndObject();
        return gen;
    }

and within any other serializer you just

            gen.writeStartArray();
            for(Embeddable embed : embedded.get(embedType)) {
                embed.serialize(gen);
            }
            gen.writeEndArray();

You must log in to answer this question.

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