Be
Kind
To Others
Designing
Developer-Friendly JSON
for API responses
{
"firstName": "John",
"lastName": "Doe"
}
class Person
{
public $firstName;
public $lastName;
}
$myBoss = new Person($json);
echo $myBoss->firstName;
echo $myBoss->lastName;
{
"first_name": "John",
"last_name": "Doe"
}
class Person
{
public $first_name;
public $last_name;
}
$myBoss = new Person($json);
echo $myBoss->first_name;
echo $myBoss->last_name;
// camelCase
{
"myList": [
{ "myName": "a" },
{ "myName": "b" },
{ "myName": "c" }
]
}
// snake_case
{
"my_list": [
{ "my_name": "a"},
{ "my_name": "b"},
{ "my_name": "c"}
]
}
echo $post->ID . $post->post_title;
{
"camelCase": 1,
"snake_case": 2,
"PascalCase": 3,
"kebab-case-sounds-tasty": 4,
"CAPS": 5,
"sUpErCaLiFrAgIlIsTiCeXpIaLiDoCiOuS": 6
}
// PHP
$x = 3;
$x = null;
// Swift
var x: Int? = nil // allowed - Optional
var x: Int = nil // not allowed
var y: Int? = 3
let a: Int = y! + 2 // allowed - unwrap Optional first
let b: Int = y + 2 // not allowed
// Java
int a = 1;
Integer b = 1;
int c = null; // not allowed
Integer d = null; // allowed
// Swift
@objc
public class Person: NSObject, Codable {
@objc public var id: Int = 0
@objc public var name: String? = nil
@objc public var address: Address? = nil
@objc public var hobbies: [String]? = nil
@objc public var age: Int? = nil // not allowed cos of @objc
}
{
"id": 3,
"name": "Bob",
"address": {
"street": "1 Alpha Ave",
"zip": 123456
},
"hobbies": ["cycling", "swimming"],
"age": null
}
{
duration: -1,
recordsPerPage: -1,
age: -1
}
/* Different checks for null and empty string */
// PHP
$s = '';
echo (! $s) ? 'empty' : 'non-empty';
$s = null;
echo (! $s) ? 'empty' : 'non-empty'; // same check as for ''
// Java
String s = "";
System.out.println(s.equals("") ? "empty" : "non-empty"); // if s is null?
s = null; // s.equals() will throw NullPointerException
System.out.println(s == null ? "empty" : "non-empty");
// Swift
var s: String? = "" // cannot use String if value can be null
print(s == "" ? "empty" : "non-empty")
s = nil
print(s == nil ? "empty" : "non-empty")
{
"id": 1,
"name": "John Doe",
"spouse": {
"id": "2",
"name": "Jane Doe"
}
}
// PHP
$x = 1;
$x = 'Hello';
// Java
int x = 1;
x = "Hello"; // not allowed
// Swift
let x: Int = 1
x = "Hello" // not allowed
{
"children": [
{
"id": 1,
"age": 3
},
{
"id": 2,
"age": 5
}
]
}
{
"children": {
"id": 1,
"age": 3
}
}
{
"children": []
}
https://example.com/products
{
products: [],
per_page: 10
}
https://example.com/products?per_page=10
{
products: [],
per_page: "10"
}
-9223372036854775808 to 9223372036854775807
0 to 18446744073709551616
123e4567-e89b-12d3-a456-426655440000
{
"name": "Alice",
"children": 3
}
{
"name": "Bob"
}
@objc public class Person: NSObject, Codable { // Swift
@objc public var id: Int = 0
@objc public var name: String? = nil
@objc public var address: Address? = nil
@objc public var children: Int = 0
// This entire init method could have been omitted
required public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decode(Int.self, forKey: .id)
name = try values.decode(String?.self, forKey: .name)
address = try values.decodeIfPresent(Address.self, forKey: .address)
do {
children = try values.decode(Int.self, forKey: .children)
} catch _ {
children = 0
}
}
}
let person = try JSONDecoder().decode(Person.self,
from: jsonString.data(using: .utf8)
)
// Sample response from Person endpoint in API: https://example.com/people/321
{
"status": "ok",
"id": 321,
"name": "John",
"address": {
"street": "Beta Ave",
"zip": 123
}
}
// Sample response from Products endpoint in API: https://example.com/products
{
"status": "ok",
"products": [
{
"id": 1,
"name": "Air"
}
],
"recordsPerPage": 1,
"page": 1,
"total": 100
}
// Swift
@objc public class PersonResponse: NSObject, Codable {
@objc public var status: String? = nil
@objc public var id: Int = 0
@objc public var name: String? = nil
@objc public var address: Address? = nil
}
@objc public class Person: NSObject, Codable {
@objc public var id: Int = 0
@objc public var name: String? = nil
@objc public var address: Address? = nil
}
@objc public class Address: NSObject, Codable {
@objc public var street: String? = nil
@objc public var zip: Int = 0
}
// Swift
@objc public class ProductsResponse: NSObject, Codable {
@objc public var status: String? = nil
@objc public var products: [Product]? = nil
@objc public var recordsPerPage: Int = 0
@objc public var page: Int = 0
@objc public var total: Int = 0
}
@objc public class Product: NSObject, Codable {
@objc public var id: Int = 0
@objc public var name: String? = nil
}
// Swift generics - T would be PersonResponse or ProductResponse
func handleResponse<T: Codable>(
completionHandler: @escaping (Response<T>) -> Void
) {
// Get JSON response from some API and save as jsonString
let response = try JSONDecoder().decode(T.self, from: jsonString.data(using: .utf8))
completionHandler(response)
}
let responseHandler = { (response: Response<PersonResponse>) -> Void in
print(response.status)
}
handleResponse(completionHandler: responseHandler)
// Suggested error response
{
"data": null,
"error": {
"message": "Person not found"
},
"pagination": null
}
// Suggested response for Person endpoint
{
"data": {
"id": 321,
"name": "John",
"address": {
"street": "Beta Ave",
"zip": 123
}
},
"error": null,
"pagination": null
}
// Suggested response for Products endpoint
{
"data": {
"products": [
{
"id": 1,
"name": "Air"
}
]
},
"error": null,
"pagination": {
"recordsPerPage": 1,
"page": 1,
"total": 100
}
}
// Response from Employee endpoint
{
"id": 1,
"lastName": "Woe",
"age": 30,
"spouse": {
"personId": 15,
"familyName": "Doe",
"years": 29
}
}
// Response from Manager endpoint
{
"employeeId": 5,
"surname": "Foe",
"howOld": 40
}
echo date('d/m/y h:i A');
// All output is in ISO8601
// 2018-08-09T00:00:00+08:00 if server timezone is set to SGT
// 2018-08-08T16:00:00+00:00 if server timezone is set to UTC
echo date('c');
echo gmdate('c'); // always in GMT/UTC - 2018-08-08T16:00:00+00:00
$now = DateTimeImmutable::createFromFormat(
'U.u',
number_format(microtime(true), 6, '.', ''),
new DateTimeZone('UTC')
);
echo $now->format('Y-m-d\TH:i:s.uP'); // in UTC - 2018-08-08T16:00:00.123456+00:00
{
"posts": [
{ "id":1, "updated":1537511106 },
{ "id":2, "updated":1537531515 },
{ "id":3, "updated":1537533015 },
{ "id":4, "updated":1540125015 },
{ "id":5, "updated":2147483647 }
]
}
{
"posts": [
{ "id":1, "updated":"2018-09-21T06:25:06+00:00" },
{ "id":2, "updated":"2018-09-21T12:05:15+00:00" },
{ "id":3, "updated":"2018-09-21T12:30:15+00:00" },
{ "id":4, "updated":"2018-10-21T12:30:15+00:00" },
{ "id":5, "updated":"2038-01-19T03:14:07+00:00" }
]
}
Which is easier to read?
// someFeature flag considered unspecified if value is -1 or flag is not set
{
"featureFlags": {
"someFeature": 1
}
}
if (featureFlags.someFeature) { // easy to check
// turn feature on
}
// `value` column uses TINYINT(1) data type
+----+-------------+-------+
| id | flag | value |
+----+-------------+-------+
| 1 | someFeature | 1 |
+----+-------------+-------+
// Using boolean values: true, false
// What value to use to indicate unspecified/default behaviour?
{
"featureFlags": {
"someFeature": true
}
}
if (featureFlags.someFeature) { // still easy to check
// turn feature on
}
// MySQL has no BOOLEAN data type and uses TINYINT(1) instead
// API needs extra code to cast 1 to true, 0 to false
+----+-------------+-------+
| id | flag | value |
+----+-------------+-------+
| 1 | someFeature | 1 |
+----+-------------+-------+
// Using string values: "on", "off", "yes", "no", "enabled", "disabled"
// What value to use to indicate unspecified/default behaviour?
{
"featureFlags": {
"someFeature": "on"
}
}
// Hard to check
if ('on' === someFeature || 'yes' === someFeature || 'enabled' === someFeature) {
// turn feature on
}
// API needs extra code to cast "on" to true, "off" to false
// Consumer of API response needs extra code to cast back "on" to true, etc.
+----+-------------+-------+
| id | flag | value |
+----+-------------+-------+
| 1 | someFeature | on |
+----+-------------+-------+
// Misspellings/miscapitalisation in code/JSON: "on", "ON", "On", "oN", "om", "no"