6

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?

zoul
  • 102,279
  • 44
  • 260
  • 354
  • 4
    Go on SwiftyJSON: https://github.com/SwiftyJSON/SwiftyJSON – Maxim Shoustin May 08 '17 at 09:02
  • I already use a different JSON decoding library and if there is no simpler solution, I guess I’d compare the values using `NSDictionary(dictionary: foo).isEqual(to: bar)` rather than switch to a different library. But I’ll take a look, thank you. – zoul May 08 '17 at 09:06
  • As noted elsewhere, SwiftyJSON is a decent option for now. But depending on where you are in the development lifecycle of your product, you might want to keep an eye on the [built-in JSON encoding coming in Swift 4](https://github.com/apple/swift-evolution/blob/master/proposals/0167-swift-encoders.md). – rickster May 08 '17 at 16:31
  • 1
    A JSON object as parsed from JSON can be a dictionary or an array (both are possible as top level objects), or, if fragments are allowed, a boolean, number or a string. That's why `Any` is used in the parser. Defining equality on dictionaries is very tricky because you need equality to be defined on values which is hard to define for `Any`. In a type-safe language defining JSON as a dictionary is not very flexible. That's why a specific JSON type is better. On a specific JSON type defining equality is trivial. I recommend using SwiftyJSON. – Sulthan May 08 '17 at 17:06

5 Answers5

6

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.

Christian Schnorr
  • 10,768
  • 8
  • 48
  • 83
  • Works wonders, thank you very much! (I have introduced a separate `GenericKey` struct to hold the key, so that I didn’t need to squat on `String`.) I didn’t find a way to represent the “only arrays and objects allowed as top-level objects” constraint in the type, but other than that it’s very nice. I’ll package the code as a library on GitHub, in case anyone is interested. Thanks again! – zoul Aug 25 '17 at 09:39
  • There's no need to make a custom `CodingKey` type (or conform `String` to it); just call the given `encode` overloads on a `singleValueContainer`, e.g https://gist.github.com/hamishknight/e5bd36a1d5868b896f09dedad51b9ee9 – Hamish Aug 25 '17 at 17:00
2

You could use generics for this:

typealias JSON<T: Any> = [String: T] where T: Equatable
zoul
  • 102,279
  • 44
  • 260
  • 354
Welton122
  • 1,101
  • 2
  • 13
  • 28
  • No idea why you’re being downvoted without a comment. But this doesn’t work: `'where' clause cannot be attached to a non-generic declaration`. Can I say `Any: Equatable` somehow? I see, it probably [doesn’t make sense in Swift](http://stackoverflow.com/questions/25901105/compare-anyobjects-in-swift-without-casting-them-to-a-specific-type)… – zoul May 08 '17 at 09:08
  • Not sure why I'm being down voted either. Generic is the way this should be done. This code works perfectly fine for me and compiles no problem. I should add, its Swift 3 code. I'm on Xcode 8.3.2 :) – Welton122 May 08 '17 at 10:04
  • That’s something different, though, isn’t it? It’s a generic type that has to be specialized before it can be used for a variable. It looks that what I want is not possible in Swift – [see here for example](https://forums.developer.apple.com/thread/11165). – zoul May 08 '17 at 13:15
2

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.

rahenri
  • 115
  • 1
  • 6
1

You could try this BeyovaJSON

import BeyovaJSON

let foo: JToken = [
    "foo": 1,
    "bar": "baz",
]

let encoded = try JSONEncoder().encode(foo)
canius
  • 93
  • 5
-1

From my point of view the most suitable is SwiftyJSON. It has nice API for the developers to be sure how parse and work with JSON objects.

JSON object have quite nice interface for working with different types of the response.

From the Apple docs:

Types that conform to the Equatable protocol can be compared for equality using the equal-to operator (==) or inequality using the not-equal-to operator (!=). Most basic types in the Swift standard library conform to Equatable.

Let consider case that you ask.We should just check if JSON conforms to protocol Equatable.

Oleg Gordiichuk
  • 15,240
  • 7
  • 60
  • 100