Last active
January 12, 2026 18:19
-
-
Save marshmallowrobot/f98a1bc8a4aa8070c0dc133011af0dc7 to your computer and use it in GitHub Desktop.
In Swift, use AnyCodable to represent dynamic JSON-schemas. This type is Codable, Equatable, and Hashable. It is also Firestore-friendly (works with NSNumber).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import FirebaseFirestore | |
| /// A Codable representation of any JSON-compatible value | |
| enum AnyCodable: Codable, Equatable, Hashable { | |
| case null | |
| case bool(Bool) | |
| case int(Int) | |
| case double(Double) | |
| case string(String) | |
| case array([AnyCodable]) | |
| case dictionary([String: AnyCodable]) | |
| // MARK: - Any initializer (Firestore / Foundation) | |
| init?(_ value: Any) { | |
| switch value { | |
| case is NSNull: | |
| self = .null | |
| case let v as Bool: | |
| self = .bool(v) | |
| case let v as Int: | |
| self = .int(v) | |
| case let v as Int64: | |
| self = .int(Int(v)) | |
| case let v as Double: | |
| self = .double(v) | |
| case let v as String: | |
| self = .string(v) | |
| case let v as [Any]: | |
| self = .array(v.compactMap { AnyCodable($0) }) | |
| case let v as [String: Any]: | |
| self = .dictionary( | |
| v.compactMapValues { AnyCodable($0) } | |
| ) | |
| case let v as NSNumber: | |
| if CFGetTypeID(v) == CFBooleanGetTypeID() { | |
| self = .bool(v.boolValue) | |
| } else if v.doubleValue.truncatingRemainder(dividingBy: 1) == 0 { | |
| self = .int(v.intValue) | |
| } else { | |
| self = .double(v.doubleValue) | |
| } | |
| default: | |
| return nil | |
| } | |
| } | |
| // MARK: - Codable | |
| init(from decoder: Decoder) throws { | |
| let container = try decoder.singleValueContainer() | |
| if container.decodeNil() { | |
| self = .null | |
| } else if let bool = try? container.decode(Bool.self) { | |
| self = .bool(bool) | |
| } else if let int = try? container.decode(Int.self) { | |
| self = .int(int) | |
| } else if let double = try? container.decode(Double.self) { | |
| self = .double(double) | |
| } else if let string = try? container.decode(String.self) { | |
| self = .string(string) | |
| } else if let array = try? container.decode([AnyCodable].self) { | |
| self = .array(array) | |
| } else if let dict = try? container.decode([String: AnyCodable].self) { | |
| self = .dictionary(dict) | |
| } else { | |
| throw DecodingError.typeMismatch( | |
| AnyCodable.self, | |
| .init( | |
| codingPath: decoder.codingPath, | |
| debugDescription: "Unsupported JSON value" | |
| ) | |
| ) | |
| } | |
| } | |
| func encode(to encoder: Encoder) throws { | |
| var container = encoder.singleValueContainer() | |
| switch self { | |
| case .null: | |
| try container.encodeNil() | |
| case .bool(let v): | |
| try container.encode(v) | |
| case .int(let v): | |
| try container.encode(v) | |
| case .double(let v): | |
| try container.encode(v) | |
| case .string(let v): | |
| try container.encode(v) | |
| case .array(let v): | |
| try container.encode(v) | |
| case .dictionary(let v): | |
| try container.encode(v) | |
| } | |
| } | |
| // MARK: - Underlying value | |
| var value: Any { | |
| switch self { | |
| case .null: | |
| return NSNull() | |
| case .bool(let v): | |
| return v | |
| case .int(let v): | |
| return v | |
| case .double(let v): | |
| return v | |
| case .string(let v): | |
| return v | |
| case .array(let v): | |
| return v.map(\.value) | |
| case .dictionary(let v): | |
| return v.mapValues(\.value) | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment