Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to represent a generic JSON structure in Swift?

I would like to represent a generic JSON object in Swift:

let foo: [String: Any] = [
    "foo": 1,
    "bar": "baz",
]

But the [String: Any] type suggested by the compiler doesn’t really work well. I can’t check two instances of the type for equality, for example, while that should be possible with two JSON trees.

What also doesn’t work is using the Codable machinery to encode that value into a JSON string:

let encoded = try JSONEncoder().encode(foo)

Which blows up with an error:

fatal error: Dictionary<String, Any> does not conform to Encodable because Any does not conform to Encodable.

I know I can introduce a precise type, but I am after a generic JSON structure. I even tried to introduce a specific type for generic JSON:

enum JSON {
    case string(String)
    case number(Float)
    case object([String:JSON])
    case array([JSON])
    case bool(Bool)
    case null
}

But when implementing Codable for this enum I don’t know how to implement encode(to:), since a keyed container (for encoding objects) requires a particular CodingKey argument and I don’t know how to get that.

Is it really not possible to create an Equatable generic JSON tree and encode it using Codable?

like image 871
zoul Avatar asked May 08 '17 09:05

zoul


3 Answers

We will use generic strings as coding keys:

extension String: CodingKey {
    public init?(stringValue: String) {
        self = stringValue
    }

    public var stringValue: String {
        return self
    }

    public init?(intValue: Int) {
        return nil
    }

    public var intValue: Int? {
        return nil
    }
}

The rest really is just a matter of getting the correct type of container and writing your values to it.

extension JSON: Encodable {
    public func encode(to encoder: Encoder) throws {
        switch self {
        case .string(let string):
            var container = encoder.singleValueContainer()
            try container.encode(string)
        case .number(let number):
            var container = encoder.singleValueContainer()
            try container.encode(number)
        case .object(let object):
            var container = encoder.container(keyedBy: String.self)

            for (key, value) in object {
                try container.encode(value, forKey: key)
            }
        case .array(let array):
            var container = encoder.unkeyedContainer()

            for value in array {
                try container.encode(value)
            }
        case .bool(let bool):
            var container = encoder.singleValueContainer()
            try container.encode(bool)
        case .null:
            var container = encoder.singleValueContainer()
            try container.encodeNil()
        }
    }
}

Given this, I'm sure you can implement Decodable and Equatable yourself.


Note that this will crash if you try to encode anything other than an array or an object as a top-level element.

like image 137
Christian Schnorr Avatar answered Oct 20 '22 06:10

Christian Schnorr


I ran into this issue but I had too many types that I want to desserialize so I think I would have to enumerate all of them in the JSON enum from the accepted answer. So I created a simple wraper which worked surprisingly well:

struct Wrapper: Encodable {
    let value: Encodable
    func encode(to encoder: Encoder) throws {
        try value.encode(to: encoder)
    }
}

then you could write

let foo: [String: Wrapper] = [
    "foo": Wrapper(value: 1),
    "bar": Wrapper(value: "baz"),
]

let encoded = try JSONEncoder().encode(foo) // now this works

Not the prettiest code ever, but worked for any type you want to encode without any additional code.

like image 2
rahenri Avatar answered Oct 20 '22 05:10

rahenri


You could use generics for this:

typealias JSON<T: Any> = [String: T] where T: Equatable
like image 1
Welton122 Avatar answered Oct 20 '22 06:10

Welton122