├── .gitignore ├── IndeterminateTypesWithCodable.playground ├── Pages │ ├── Open-Closed.xcplaygroundpage │ │ └── Contents.swift │ └── Using Enum.xcplaygroundpage │ │ └── Contents.swift └── contents.xcplayground ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | .build/ 41 | 42 | # CocoaPods 43 | # 44 | # We recommend against adding the Pods directory to your .gitignore. However 45 | # you should judge for yourself, the pros and cons are mentioned at: 46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 47 | # 48 | # Pods/ 49 | 50 | # Carthage 51 | # 52 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 53 | # Carthage/Checkouts 54 | 55 | Carthage/Build 56 | 57 | # fastlane 58 | # 59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 60 | # screenshots whenever they are needed. 61 | # For more information about the recommended setup visit: 62 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 63 | 64 | fastlane/report.xml 65 | fastlane/Preview.html 66 | fastlane/screenshots 67 | fastlane/test_output 68 | -------------------------------------------------------------------------------- /IndeterminateTypesWithCodable.playground/Pages/Open-Closed.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct ImageAttachment: Codable { 4 | let url: URL 5 | let width: Int 6 | let height: Int 7 | } 8 | 9 | struct AudioAttachment: Codable { 10 | let title: String 11 | let url: URL 12 | let shouldAutoplay: Bool 13 | } 14 | 15 | struct Attachment: Codable { 16 | let type: String 17 | let payload: Any? 18 | 19 | // MARK: Codable 20 | 21 | private enum CodingKeys: String, CodingKey { 22 | case type 23 | case payload 24 | } 25 | 26 | init(from decoder: Decoder) throws { 27 | let container = try decoder.container(keyedBy: CodingKeys.self) 28 | type = try container.decode(String.self, forKey: .type) 29 | 30 | if let decode = Attachment.decoders[type] { 31 | payload = try decode(container) 32 | } else { 33 | payload = nil 34 | } 35 | } 36 | 37 | func encode(to encoder: Encoder) throws { 38 | var container = encoder.container(keyedBy: CodingKeys.self) 39 | 40 | try container.encode(type, forKey: .type) 41 | 42 | if let payload = self.payload { 43 | guard let encode = Attachment.encoders[type] else { 44 | let context = EncodingError.Context(codingPath: [], debugDescription: "Invalid attachment: \(type).") 45 | throw EncodingError.invalidValue(self, context) 46 | } 47 | 48 | try encode(payload, &container) 49 | } else { 50 | try container.encodeNil(forKey: .payload) 51 | } 52 | } 53 | 54 | // MARK: Registration 55 | 56 | private typealias AttachmentDecoder = (KeyedDecodingContainer) throws -> Any 57 | private typealias AttachmentEncoder = (Any, inout KeyedEncodingContainer) throws -> Void 58 | 59 | private static var decoders: [String: AttachmentDecoder] = [:] 60 | private static var encoders: [String: AttachmentEncoder] = [:] 61 | 62 | static func register(_ type: A.Type, for typeName: String) { 63 | decoders[typeName] = { container in 64 | try container.decode(A.self, forKey: .payload) 65 | } 66 | 67 | encoders[typeName] = { payload, container in 68 | try container.encode(payload as! A, forKey: .payload) 69 | } 70 | } 71 | } 72 | 73 | struct Message: Codable { 74 | let from: String 75 | let text: String 76 | let attachments: [Attachment] 77 | } 78 | 79 | Attachment.register(ImageAttachment.self, for: "image") 80 | Attachment.register(AudioAttachment.self, for: "audio") 81 | 82 | let json = """ 83 | { 84 | "from": "Guille", 85 | "text": "Look what I just found!", 86 | "attachments": [ 87 | { 88 | "type": "image", 89 | "payload": { 90 | "url": "http://via.placeholder.com/640x480", 91 | "width": 640, 92 | "height": 480 93 | } 94 | }, 95 | { 96 | "type": "audio", 97 | "payload": { 98 | "title": "Never Gonna Give You Up", 99 | "url": "https://contoso.com/media/NeverGonnaGiveYouUp.mp3", 100 | "shouldAutoplay": true, 101 | } 102 | }, 103 | { 104 | "type": "pdf", 105 | "payload": { 106 | "title": "The Swift Programming Language", 107 | "url": "https://contoso.com/media/SwiftBook.pdf" 108 | } 109 | } 110 | ] 111 | } 112 | """.data(using: .utf8)! 113 | 114 | let decoder = JSONDecoder() 115 | let message = try decoder.decode(Message.self, from: json) 116 | 117 | print("\(message.from) says: '\(message.text)'") 118 | 119 | for attachment in message.attachments { 120 | switch attachment.payload { 121 | case let image as ImageAttachment: 122 | print("found 'image' at \(image.url) with size \(image.width)x\(image.height)") 123 | case let audio as AudioAttachment: 124 | print("found 'audio' titled \"\(audio.title)\"") 125 | default: 126 | print("unsupported attachment: \(attachment.type)") 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /IndeterminateTypesWithCodable.playground/Pages/Using Enum.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct ImageAttachment: Codable { 4 | let url: URL 5 | let width: Int 6 | let height: Int 7 | } 8 | 9 | struct AudioAttachment: Codable { 10 | let title: String 11 | let url: URL 12 | let shouldAutoplay: Bool 13 | } 14 | 15 | enum Attachment { 16 | case image(ImageAttachment) 17 | case audio(AudioAttachment) 18 | case unsupported 19 | } 20 | 21 | extension Attachment: Codable { 22 | private enum CodingKeys: String, CodingKey { 23 | case type 24 | case payload 25 | } 26 | 27 | init(from decoder: Decoder) throws { 28 | let container = try decoder.container(keyedBy: CodingKeys.self) 29 | let type = try container.decode(String.self, forKey: .type) 30 | 31 | switch type { 32 | case "image": 33 | let payload = try container.decode(ImageAttachment.self, forKey: .payload) 34 | self = .image(payload) 35 | case "audio": 36 | let payload = try container.decode(AudioAttachment.self, forKey: .payload) 37 | self = .audio(payload) 38 | default: 39 | self = .unsupported 40 | } 41 | } 42 | 43 | func encode(to encoder: Encoder) throws { 44 | var container = encoder.container(keyedBy: CodingKeys.self) 45 | 46 | switch self { 47 | case .image(let attachment): 48 | try container.encode("image", forKey: .type) 49 | try container.encode(attachment, forKey: .payload) 50 | case .audio(let attachment): 51 | try container.encode("audio", forKey: .type) 52 | try container.encode(attachment, forKey: .payload) 53 | case .unsupported: 54 | let context = EncodingError.Context(codingPath: [], debugDescription: "Invalid attachment.") 55 | throw EncodingError.invalidValue(self, context) 56 | } 57 | } 58 | } 59 | 60 | struct Message: Codable { 61 | let from: String 62 | let text: String 63 | let attachments: [Attachment] 64 | } 65 | 66 | let json = """ 67 | { 68 | "from": "Guille", 69 | "text": "Look what I just found!", 70 | "attachments": [ 71 | { 72 | "type": "image", 73 | "payload": { 74 | "url": "http://via.placeholder.com/640x480", 75 | "width": 640, 76 | "height": 480 77 | } 78 | }, 79 | { 80 | "type": "audio", 81 | "payload": { 82 | "title": "Never Gonna Give You Up", 83 | "url": "https://contoso.com/media/NeverGonnaGiveYouUp.mp3", 84 | "shouldAutoplay": true, 85 | } 86 | } 87 | ] 88 | } 89 | """.data(using: .utf8)! 90 | 91 | let decoder = JSONDecoder() 92 | let message = try decoder.decode(Message.self, from: json) 93 | 94 | print("\(message.from) says: '\(message.text)'") 95 | 96 | for attachment in message.attachments { 97 | switch attachment { 98 | case .image(let image): 99 | print("found 'image' at \(image.url) with size \(image.width)x\(image.height)") 100 | case .audio(let audio): 101 | print("found 'audio' titled \"\(audio.title)\"") 102 | case .unsupported: 103 | print("unsupported") 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /IndeterminateTypesWithCodable.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Guille Gonzalez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Indeterminate Types with Codable in Swift 2 | This playground explores two different approaches to decode or encode JSON containing objects whose type is determined by a property. 3 | 4 | ```json 5 | { 6 | "from": "Guille", 7 | "text": "Look what I just found!", 8 | "attachments": [ 9 | { 10 | "type": "image", 11 | "payload": { 12 | "url": "http://via.placeholder.com/640x480", 13 | "width": 640, 14 | "height": 480 15 | } 16 | }, 17 | { 18 | "type": "audio", 19 | "payload": { 20 | "title": "Never Gonna Give You Up", 21 | "url": "https://audio.com/NeverGonnaGiveYouUp.mp3", 22 | "shouldAutoplay": true, 23 | } 24 | } 25 | ] 26 | } 27 | ``` 28 | 29 | You can find the full blog post [here](https://gonzalezreal.github.io/2018/04/30/indeterminate-types-with-codable-in-swift.html). 30 | --------------------------------------------------------------------------------