7

With Swift 3 JSONSerialization, if a part of your data model was completely dynamic, you could always just leave the deserialized data at Any and let the consumer deal with it.

With Swift 4's Codable, I'd like to do this:

struct Foo : Codable {
    let bar: Any;
}

But I get

JSONPlayground.playground:4:9: note: cannot automatically synthesize 'Decodable' because 'Any' does not conform to 'Decodable'
    let bar: Any;
        ^

That alone wouldn't be the end of the world if I could implement my own Decodable, but that would require the Decoder to support decoding to Any, which as far as I can tell it does not. For example:

extension Foo {
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let result = try container.decode(AnyClass.self)
    }
}

gives

error: JSONPlayground.playground:4:36: error: cannot invoke 'decode' with an argument list of type '(AnyClass.Protocol)'
        let result = try container.decode(AnyClass.self)
                                   ^

Is there any solution to this?

Mark Probst
  • 7,107
  • 7
  • 40
  • 42
  • Think you might be out of luck with trying to decode `Any` but maybe you can just try converting bar to a JSON string and then store it that way – TNguyen Sep 18 '17 at 14:03
  • `Codable` won't decode a number to a string, though. – Mark Probst Sep 18 '17 at 14:21
  • Why do you need `Any`? – Lorenzo B Sep 18 '17 at 14:24
  • 1
    Compare [How to represent a generic JSON structure in Swift?](https://stackoverflow.com/q/43843604/2976878) – Hamish Sep 18 '17 at 14:57
  • 1
    @LorenzoB I'm working on [quicktype](https://quicktype.io), a tool for generating static types and serializers/deserializers for a number of programming languages, from JSON data. In some cases we can't infer a type from the data (for details on that, see [my blog post](http://blog.quicktype.io/under-the-hood/)), so we default to the target language's equivalent of `Any`. – Mark Probst Sep 18 '17 at 15:17
  • @MarkProbst Thanks for the clarification. Interesting tool. Let me think on a possible solution. – Lorenzo B Sep 18 '17 at 15:58
  • 1
    If you don't at all know the structure of the data in the JSON is at a given point, then you're out of luck at the moment as this isn't yet possible to do efficiently/in a reasonable manner. See https://bugs.swift.org/browse/SR-5311 – Itai Ferber Sep 18 '17 at 23:30

4 Answers4

17

I ended up having to implement my own class to encode/decode Any values. It's not pretty, but it seems to work:

class JSONAny: Codable {
    public let value: Any

    static func decodingError(forCodingPath codingPath: [CodingKey]) -> DecodingError {
        let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode JSONAny")
        return DecodingError.typeMismatch(JSONAny.self, context)
    }

    static func encodingError(forValue value: Any, codingPath: [CodingKey]) -> EncodingError {
        let context = EncodingError.Context(codingPath: codingPath, debugDescription: "Cannot encode JSONAny")
        return EncodingError.invalidValue(value, context)
    }

    static func decode(from container: SingleValueDecodingContainer) throws -> Any {
        if let value = try? container.decode(Bool.self) {
            return value
        }
        if let value = try? container.decode(Int64.self) {
            return value
        }
        if let value = try? container.decode(Double.self) {
            return value
        }
        if let value = try? container.decode(String.self) {
            return value
        }
        if container.decodeNil() {
            return JSONNull()
        }
        throw decodingError(forCodingPath: container.codingPath)
    }

    static func decode(from container: inout UnkeyedDecodingContainer) throws -> Any {
        if let value = try? container.decode(Bool.self) {
            return value
        }
        if let value = try? container.decode(Int64.self) {
            return value
        }
        if let value = try? container.decode(Double.self) {
            return value
        }
        if let value = try? container.decode(String.self) {
            return value
        }
        if let value = try? container.decodeNil() {
            if value {
                return JSONNull()
            }
        }
        if var container = try? container.nestedUnkeyedContainer() {
            return try decodeArray(from: &container)
        }
        if var container = try? container.nestedContainer(keyedBy: MyCodingKey.self) {
            return try decodeDictionary(from: &container)
        }
        throw decodingError(forCodingPath: container.codingPath)
    }

    static func decode(from container: inout KeyedDecodingContainer, forKey key: MyCodingKey) throws -> Any {
        if let value = try? container.decode(Bool.self, forKey: key) {
            return value
        }
        if let value = try? container.decode(Int64.self, forKey: key) {
            return value
        }
        if let value = try? container.decode(Double.self, forKey: key) {
            return value
        }
        if let value = try? container.decode(String.self, forKey: key) {
            return value
        }
        if let value = try? container.decodeNil(forKey: key) {
            if value {
                return JSONNull()
            }
        }
        if var container = try? container.nestedUnkeyedContainer(forKey: key) {
            return try decodeArray(from: &container)
        }
        if var container = try? container.nestedContainer(keyedBy: MyCodingKey.self, forKey: key) {
            return try decodeDictionary(from: &container)
        }
        throw decodingError(forCodingPath: container.codingPath)
    }

    static func decodeArray(from container: inout UnkeyedDecodingContainer) throws -> [Any] {
        var arr: [Any] = []
        while !container.isAtEnd {
            let value = try decode(from: &container)
            arr.append(value)
        }
        return arr
    }

    static func decodeDictionary(from container: inout KeyedDecodingContainer) throws -> [String: Any] {
        var dict = [String: Any]()
        for key in container.allKeys {
            let value = try decode(from: &container, forKey: key)
            dict[key.stringValue] = value
        }
        return dict
    }

    static func encode(to container: inout UnkeyedEncodingContainer, array: [Any]) throws {
        for value in array {
            if let value = value as? Bool {
                try container.encode(value)
            } else if let value = value as? Int64 {
                try container.encode(value)
            } else if let value = value as? Double {
                try container.encode(value)
            } else if let value = value as? String {
                try container.encode(value)
            } else if value is JSONNull {
                try container.encodeNil()
            } else if let value = value as? [Any] {
                var container = container.nestedUnkeyedContainer()
                try encode(to: &container, array: value)
            } else if let value = value as? [String: Any] {
                var container = container.nestedContainer(keyedBy: MyCodingKey.self)
                try encode(to: &container, dictionary: value)
            } else {
                throw encodingError(forValue: value, codingPath: container.codingPath)
            }
        }
    }

    static func encode(to container: inout KeyedEncodingContainer, dictionary: [String: Any]) throws {
        for (key, value) in dictionary {
            let key = MyCodingKey(stringValue: key)!
            if let value = value as? Bool {
                try container.encode(value, forKey: key)
            } else if let value = value as? Int64 {
                try container.encode(value, forKey: key)
            } else if let value = value as? Double {
                try container.encode(value, forKey: key)
            } else if let value = value as? String {
                try container.encode(value, forKey: key)
            } else if value is JSONNull {
                try container.encodeNil(forKey: key)
            } else if let value = value as? [Any] {
                var container = container.nestedUnkeyedContainer(forKey: key)
                try encode(to: &container, array: value)
            } else if let value = value as? [String: Any] {
                var container = container.nestedContainer(keyedBy: MyCodingKey.self, forKey: key)
                try encode(to: &container, dictionary: value)
            } else {
                throw encodingError(forValue: value, codingPath: container.codingPath)
            }
        }
    }

    static func encode(to container: inout SingleValueEncodingContainer, value: Any) throws {
        if let value = value as? Bool {
            try container.encode(value)
        } else if let value = value as? Int64 {
            try container.encode(value)
        } else if let value = value as? Double {
            try container.encode(value)
        } else if let value = value as? String {
            try container.encode(value)
        } else if value is JSONNull {
            try container.encodeNil()
        } else {
            throw encodingError(forValue: value, codingPath: container.codingPath)
        }
    }

    public required init(from decoder: Decoder) throws {
        if var arrayContainer = try? decoder.unkeyedContainer() {
            self.value = try JSONAny.decodeArray(from: &arrayContainer)
        } else if var container = try? decoder.container(keyedBy: MyCodingKey.self) {
            self.value = try JSONAny.decodeDictionary(from: &container)
        } else {
            let container = try decoder.singleValueContainer()
            self.value = try JSONAny.decode(from: container)
        }
    }

    public func encode(to encoder: Encoder) throws {
        if let arr = self.value as? [Any] {
            var container = encoder.unkeyedContainer()
            try JSONAny.encode(to: &container, array: arr)
        } else if let dict = self.value as? [String: Any] {
            var container = encoder.container(keyedBy: MyCodingKey.self)
            try JSONAny.encode(to: &container, dictionary: dict)
        } else {
            var container = encoder.singleValueContainer()
            try JSONAny.encode(to: &container, value: self.value)
        }
    }
}

class JSONNull: Codable {
    public init() {
    }

    public required init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if !container.decodeNil() {
            throw DecodingError.typeMismatch(JSONNull.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for JSONNull"))
        }
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encodeNil()
    }
}

class MyCodingKey : CodingKey {
    let key: String

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

    required init?(stringValue: String) {
        key = stringValue
    }

    var intValue: Int? {
        return nil
    }

    var stringValue: String {
        return key
    }
}
scum
  • 3,202
  • 1
  • 29
  • 27
Mark Probst
  • 7,107
  • 7
  • 40
  • 42
  • 1
    This is great, thank you. I only had to add to KeyedEncodingContainer, which caused build errors on its own. – Yaroslav Oct 01 '17 at 12:17
  • Thanks for this, it works great for parsing JSON, I was wondering though, could you tell me how to write a single value to an existing JSONAny? – kevin Jul 12 '18 at 13:14
  • 1
    Does this work in Swift 5? I'm getting XCode complaining about `Reference to generic type 'KeyedDecodingContainer' requires arguments in <...>`, and trying to "fix" it with invalid fixes – John Kaster Feb 13 '20 at 21:42
9

Simply you can use AnyCodable type from Matt Thompson's cool library AnyCodable.

Eg:

import AnyCodable

struct Foo: Codable
{
    var bar: AnyCodable
}
Johnykutty
  • 12,091
  • 13
  • 59
  • 100
3

I solved my problem creating an AnyValue struct to allow Encode and Decode Any values from JSON:

To use it is pretty simple:

class MyClass: Codable {

    var data: [String: AnyValue?]?

    init(data: [String: AnyValue?]) {
        self.data = data
    }
}


let data = ["a": AnyValue(3), "b": AnyValue(true), "c": AnyValue("Rodrigo"), "d": AnyValue(3.3)]
let myClass = MyClass(data: data)

if let json = JsonUtil<MyClass>.toJson(myClass) {
    print(json) // {"data":{"d":3.3,"b":true,"c":"Rodrigo","a":3}}

    if let data = JsonUtil<MyClass>.from(json: json)?.data {
        print(data["a"]??.value() ?? "nil") // 3
        print(data["b"]??.value() ?? "nil") // true
        print(data["c"]??.value() ?? "nil") // Rodrigo
        print(data["d"]??.value() ?? "nil") // 3.3
    }
}


AnyValue struct:

struct AnyValue: Codable {

    private var int: Int?
    private var string: String?
    private var bool: Bool?
    private var double: Double?

    init(_ int: Int) {
        self.int = int
    }

    init(_ string: String) {
        self.string = string
    }

    init(_ bool: Bool) {
        self.bool = bool
    }

    init(_ double: Double) {
        self.double = double
    }

    init(from decoder: Decoder) throws {
        if let int = try? decoder.singleValueContainer().decode(Int.self) {
            self.int = int
            return
        }

        if let string = try? decoder.singleValueContainer().decode(String.self) {
            self.string = string
            return
        }

        if let bool = try? decoder.singleValueContainer().decode(Bool.self) {
            self.bool = bool
            return
        }

        if let double = try? decoder.singleValueContainer().decode(Double.self) {
            self.double = double
        }
    }


    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()

        if let anyValue = self.value() {
            if let value = anyValue as? Int {
                try container.encode(value)
                return
            }

            if let value = anyValue as? String {
                try container.encode(value)
                return
            }

            if let value = anyValue as? Bool {
                try container.encode(value)
                return
            }

            if let value = anyValue as? Double {
                try container.encode(value)
                return
            }
        }

        try container.encodeNil()
    }


    func value() -> Any? {
        return self.int ?? self.string ?? self.bool ?? self.double
    }
}


And I created a JsonUtil struct too:

struct JsonUtil<T: Codable>  {

    public static func from(json: String) -> T? {
        if let jsonData = json.data(using: .utf8) {
            let jsonDecoder = JSONDecoder()

            do {
                return try jsonDecoder.decode(T.self, from: jsonData)

            } catch {
                print(error)
            }
        }

        return nil
    }

    public static func toJson(_ obj: T) -> String? {
        let jsonEncoder = JSONEncoder()

        do {
            let jsonData = try jsonEncoder.encode(obj)
            return String(data: jsonData, encoding: String.Encoding.utf8)

        } catch {
            print(error)
            return nil
        }
    }
}

If you need a new type like [String] per example, you just need add it on AnyValue struct.

Good luck :)

Busata
  • 1,029
  • 1
  • 13
  • 27
  • This negates all advantages of the `Codable` protocol and could be extremely expensive for large data sets. – vadian Aug 07 '18 at 15:14
  • It's a misuse of generics. An almost all cases a JSON string is not generic. – vadian Aug 07 '18 at 15:27
  • It's also using a product type (a struct) instead of a sum type (an enum). You'll be constantly unwrapping optionals, and overall have a bad time – Alexander Aug 13 '20 at 14:56
1

You could try BeyovaJSON

import BeyovaJSON

struct Foo: Codable {
  let bar: JToken
}

let foo = try! JSONDecoder().decode(Foo.self, from: data)
canius
  • 93
  • 5