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.

  • 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 '16 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. – Christian Carter Jan 5 '16 at 4:58
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();

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.