An easy way to convert Swift structs to JSON

Written by Emil Loer on May 24, 2016 |

One of the real powers of Swift is that it allows you to define custom value types by using structs. However, sometimes you also need to send these types off to some external web service (e.g. as JSON) and this is where things get tricky. To convert things to JSON we can use Foundation's NSJSONSerialization. Unfortunately this only works if whatever you are converting is an NSArray or NSDictionary so it can't be used directly.

In this article I propose a solution that is able to convert any Swift struct into a JSON object by just adding a protocol to the struct declaration. A protocol extension and clever use of the Swift reflection API does the rest. Let's see how it works.

Representable

The first thing we have to do is to define a protocol that tells us how the conforming type can be represented as input compatible to NSJSONSerialization. We will use this protocol later to easily convert our own types to their JSON representation.

protocol JSONRepresentable {
    var JSONRepresentation: AnyObject { get }
}

The result of the JSONRepresentation computed property may be one of the following: NSNull, NSString, NSNumber, NSArray, (with values mentioned in this list) or NSDictionary (with NSString keys and values mentioned in this list).

Serializable

The next step is to define a protocol that indicates that the conforming struct can be serialized.

protocol JSONSerializable: JSONRepresentable {
}

To convert the struct we have to first provide its JSON representation by implementing JSONRepresentable. This is done with a protocol extension.

extension JSONSerializable {
    var JSONRepresentation: AnyObject {
        var representation = [String: AnyObject]()

        for case let (label?, value) in Mirror(reflecting: self).children {
            switch value {
                case let value as JSONRepresentable:
                    representation[label] = value.JSONRepresentation

                case let value as NSObject:
                    representation[label] = value

                default:
                    // Ignore any unserializable properties
                    break
            }
        }

        return representation
    }
}

The trick here is that we are using the Swift reflection API (the Mirror struct) to discover and iterate over the list of properties in the struct. If the property itself conforms to JSONRepresentable then we return its JSON representation (and thus implement nested struct serialization). If not then we check if the property can be cast as an NSObject and output that in the resulting dictionary. Note that for the sake of this example's simplicity we are not checking explicitly whether the property is one of the types allowed by NSJSONSerialization. This is left as a (somewhat trivial) exercise to the reader.

Now that we have a way to represent the struct in JSON we actually have to provide some code to serialize it. This can be done with a second method in the protocol extension:

extension JSONSerializable {
    func toJSON() -> String? {
        let representation = JSONRepresentation

        guard NSJSONSerialization.isValidJSONObject(representation) else {
            return nil
        }

        do {
            let data = try NSJSONSerialization.dataWithJSONObject(representation, options: [])
            return String(data: data, encoding: NSUTF8StringEncoding)
        } catch {
            return nil
        }
    }
}

The toJSON function basically checks if the JSON representation of itself is valid and then serializes it and returns the result as a string. The function returns nil when the struct is not serializable.

Protocol usage

Consider the following example structs:

struct Owner {
    var name: String
}

struct Car {
    var manufacturer: String
    var model: String
    var mileage: Float

    var owner: Owner
}

Suppose we want to serialize instances of these structs into JSON. The only thing we have to do is to add JSONSerializable as a protocol to the struct and start using it. Just like this:

struct Owner: JSONSerializable {
    var name: String
}

struct Car: JSONSerializable {
    var manufacturer: String
    var model: String
    var mileage: Float

    var owner: Owner
}

let car = Car(manufacturer: "Tesla", model: "Model T", mileage: 1234.56, owner: Owner(name: "Emil"))

if let json = car.toJSON() {
    print(json)
}

This prints the following line:

{"owner":{"name":"Emil"},"manufacturer":"Tesla","mileage":1234.56,"model":"Model T"}

And presto! Super simple JSON struct serialization!

Bonus: adding JSON representations

The serialization function given above works perfectly for structs and the types supported by NSJSONSerialization. But what to do if a property is of another type, e.g. NSDate?

Well, we can add serialization for any other type by just conforming to JSONRepresentable. This is how to do it:

extension NSDate: JSONRepresentable {
    var JSONRepresentation: AnyObject {
        let formatter = NSDateFormatter()
        formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS"

        return formatter.stringFromDate(self)
    }
}

And now you can also serialize structs with dates:

struct Event: JSONSerializable {
    var name: String
    var timestamp: NSDate
}

let event = Event(name: "Something happened", timestamp: NSDate())

if let json = event.toJSON() {
    print(json)
}

This prints the following output:

{"timestamp":"2016-05-26T17:47:03.709","name":"Something happened"}

Final thoughts

In this article I've shown you how to serialize your own structs to JSON without heavy modifications to the structs themselves, keeping their code nice and clean. I hope it may be of use in your Swift endeavours.

If you've enjoyed this post please follow me on Twitter or Facebook. I'd really appreciate it!