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.
replaceAll()
on the resulting string to get the_....
names you need before you submit the JSON?_
members so I SHOULD be ok.