How to exclude properties from Swift Codable? - json

Swift's Encodable/Decodable protocols, released with Swift 4, make JSON (de)serialization quite pleasant. However, I have not yet found a way to have fine-grained control over which properties should be encoded and which should get decoded.
I have noticed that excluding the property from the accompanying CodingKeys enum excludes the property from the process altogether, but is there a way to have more fine-grained control?

The list of keys to encode/decode is controlled by a type called CodingKeys (note the s at the end). The compiler can synthesize this for you but can always override that.
Let's say you want to exclude the property nickname from both encoding and decoding:
struct Person: Codable {
var firstName: String
var lastName: String
var nickname: String?
private enum CodingKeys: String, CodingKey {
case firstName, lastName
}
}
If you want it to be asymmetric (i.e. encode but not decode or vice versa), you have to provide your own implementations of encode(with encoder: ) and init(from decoder: ):
struct Person: Codable {
var firstName: String
var lastName: String
// Since fullName is a computed property, it's excluded by default
var fullName: String {
return firstName + " " + lastName
}
private enum CodingKeys: String, CodingKey {
case firstName, lastName, fullName
}
// We don't want to decode `fullName` from the JSON
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
firstName = try container.decode(String.self, forKey: .firstName)
lastName = try container.decode(String.self, forKey: .lastName)
}
// But we want to store `fullName` in the JSON anyhow
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(firstName, forKey: .firstName)
try container.encode(lastName, forKey: .lastName)
try container.encode(fullName, forKey: .fullName)
}
}

Solution with custom property wrapper
struct Person: Codable {
var firstName: String
var lastName: String
#CodableIgnored
var nickname: String?
}
Where CodableIgnored is
#propertyWrapper
public struct CodableIgnored<T>: Codable {
public var wrappedValue: T?
public init(wrappedValue: T?) {
self.wrappedValue = wrappedValue
}
public init(from decoder: Decoder) throws {
self.wrappedValue = nil
}
public func encode(to encoder: Encoder) throws {
// Do nothing
}
}
extension KeyedDecodingContainer {
public func decode<T>(
_ type: CodableIgnored<T>.Type,
forKey key: Self.Key) throws -> CodableIgnored<T>
{
return CodableIgnored(wrappedValue: nil)
}
}
extension KeyedEncodingContainer {
public mutating func encode<T>(
_ value: CodableIgnored<T>,
forKey key: KeyedEncodingContainer<K>.Key) throws
{
// Do nothing
}
}

Another way to exclude some properties from encoder, separate coding container can be used
struct Person: Codable {
let firstName: String
let lastName: String
let excludedFromEncoder: String
private enum CodingKeys: String, CodingKey {
case firstName
case lastName
}
private enum AdditionalCodingKeys: String, CodingKey {
case excludedFromEncoder
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let anotherContainer = try decoder.container(keyedBy: AdditionalCodingKeys.self)
firstName = try container.decode(String.self, forKey: .firstName)
lastName = try container.decode(String.self, forKey: .lastName)
excludedFromEncoder = try anotherContainer(String.self, forKey: . excludedFromEncoder)
}
// it is not necessary to implement custom encoding
// func encode(to encoder: Encoder) throws
// let person = Person(firstName: "fname", lastName: "lname", excludedFromEncoder: "only for decoding")
// let jsonData = try JSONEncoder().encode(person)
// let jsonString = String(data: jsonData, encoding: .utf8)
// jsonString --> {"firstName": "fname", "lastName": "lname"}
}
same approach can be used for decoder

If we need to exclude decoding of a couple of properties from a large set of properties in the structure, declare them as optional properties. Code to unwrapping optionals is less than writing a lot of keys under CodingKey enum.
I would recommend using extensions to add computed instance properties and computed type properties. It separates codable comforming properties from other logic hence provides better readability.

You can use computed properties:
struct Person: Codable {
var firstName: String
var lastName: String
var nickname: String?
var nick: String {
get {
nickname ?? ""
}
}
private enum CodingKeys: String, CodingKey {
case firstName, lastName
}
}

While this can be done it ultimately ends up being very unSwifty and even unJSONy. I think I see where you are coming from, the concept of #ids is prevalent in HTML, but it is rarely transported over to the world of JSON which I consider a good thing (TM).
Some Codable structs will be able to parse your JSON file just fine if you restructure it using recursive hashes, i.e. if your recipe just contains an array of ingredients which in turn contains (one or several) ingredient_info. That way the parser will help you to stitch your network together in the first place and you only have to provide some backlinks through a simple traversal the structure if you really need them. Since this requires a thorough rework of your JSONand your data structure I only sketch out the idea for you to think about it. If you deem it acceptable please tell me in the comments then I could elaborate it further, but depending on the circumstances you may not be at the liberty to change either one of them.

I have used protocol and its extension along with AssociatedObject to set and get image (or any property which needs to be excluded from Codable) property.
With this we dont have to implement our own Encoder and Decoder
Here is the code, keeping relevant code for simplicity:
protocol SCAttachmentModelProtocol{
var image:UIImage? {get set}
var anotherProperty:Int {get set}
}
extension SCAttachmentModelProtocol where Self: SCAttachmentUploadRequestModel{
var image:UIImage? {
set{
//Use associated object property to set it
}
get{
//Use associated object property to get it
}
}
}
class SCAttachmentUploadRequestModel : SCAttachmentModelProtocol, Codable{
var anotherProperty:Int
}
Now, whenever we want to access the Image property we can use on the object confirming to protocol (SCAttachmentModelProtocol)

Related

Type ' ' does not conform to protocol 'Encodable'

Can someone tell what's wrong here please?
It send me this error: Type 'CityWeatherInfo' does not conform to protocol 'Encodable'
struct CityWeatherInfo: Codable {
var name: String
var main: Main
var weathers: [Weather]
private enum CodingKeys: String, CodingKey {
case weathers = "weather"
case main = "main"
case name
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
let mainContainer = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .main)
let weatherContainer = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .weathers)
}
}
struct Weather: Decodable {
var main: String
var description: String
private enum WeatherKeys: String, CodingKey {
case main = "main"
case description = "description"
}
}
struct Main: Decodable {
var temp: Double
var feels_like: Double
var temp_min: Double
var temp_max: Double
private enum MainKeys: String, CodingKey {
case temp = "temp"
case feels_like = "feels_like"
case temp_min = "temp_min"
case temo_max = "temp_max"
}
}
Json is this:
{"coord":{"lon":-0.13,"lat":51.51},"weather":[{"id":804,"main":"Clouds","description":"overcast clouds","icon":"04n"}],"base":"stations","main":{"temp":287.45,"feels_like":286.61,"temp_min":284.82,"temp_max":289.15,"pressure":1012,"humidity":72},"visibility":10000,"wind":{"speed":1,"deg":0},"clouds":{"all":100},"dt":1592362322,"sys":{"type":1,"id":1414,"country":"GB","sunrise":1592365362,"sunset":1592425222},"timezone":3600,"id":2643743,"name":"London","cod":200}
If a struct or class adopts Codable the required methods init(from decoder: Decoder) and encode(to encoder: Encoder) are synthesized by default.
But if you implement one of the methods yourself you have to implement the other, too. Or – in this case – adopt only Decodable because you are only reading/decoding the data.
There are a few bad practices in your code.
The custom CodingKeys WeatherKeys and MainKeys are pointless because in the default syntax the framework generates the keys named CodingKeys. Custom keys are considered only in a custom implementation.
Both nestedContainers are not needed and they make no sense anyway if they are keyed by the same CodingKeys.
You can omit the CodingKeys if the dictionary keys match the struct member names.
If the values of the struct members are not going to be modified declare them as constants (let).
According to the naming convention variables are supposed to be named lowerCamelCased. A convenient way to translate snake_case to camelCase is to specify the convertFromSnakeCase key decoding strategy.
The openweathermap API provides degrees in Celsius by adding units=metric in the URL or Fahrenheit by adding units=imperial. For example
https://api.openweathermap.org/data/2.5/weather?q=London&units=metric&appid=•••••••••••••"
The UNIX timestamps (1592362322) can be decoded as Date by specifying the secondsSince1970 date decoding strategy
The structs can be created in this simple form
struct CityWeatherInfo: Decodable {
let name: String
let main: Main
let weather: [Weather]
let dt : Date
}
struct Weather: Decodable {
let main: String
let description: String
}
struct Main: Decodable {
let temp, tempMin, tempMax, feelsLike : Double
}
And decode the data with this code
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .secondsSince1970
let weatherInfo = try decoder.decode(CityWeatherInfo.self, from: data)
print(weatherInfo)
} catch {
print(error)
}

How should I decode a json object using JSONDecoder if I am unsure of the keys

I have an api response in the following shape -
{
"textEntries":{
"summary":{
"id":"101e9136-efd9-469e-9848-132023d51fb1",
"text":"some text",
"locale":"en_GB"
},
"body":{
"id":"3692b0ec-5b92-4ab1-bc25-7711499901c5",
"text":"some other text",
"locale":"en_GB"
},
"title":{
"id":"45595d27-7e06-491e-890b-f50a5af1cdfe",
"text":"some more text again",
"locale":"en_GB"
}
}
}
I'd like to decode this via JSONDecoder so I can use the properties. The challenge I have is the keys, in this case summary,body and title are generated elsewhere and not always these values, they are always unique, but are based on logic that takes place elsewhere in the product, so another call for a different content article could return leftBody or subTitle etc.
The model for the body of these props is always the same however, I can expect the same fields to exist on any combination of responses.
I will need to be able to access the body of each key in code elsewhere. Another API response will tell me the key I need though.
I am not sure how I can handle this with Decodable as I cannot type the values ahead of time.
I had considered something like modelling the body -
struct ContentArticleTextEntries: Decodable {
var id: String
var text: String
var locale: Locale
}
and storing the values in a struct like -
struct ContentArticle: Decodable {
var textEntries: [String: ContentArticleTextEntries]
private enum CodingKeys: String, CodingKey {
case textEntries
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.textEntries = try values.decode(ContentArticleTextEntries.self, forKey: .textEntries)
}
}
I could them maybe use a subscript elsewhere to access property however I do not know how to decode into this shape as the above would not work.
So I would later access like textEntries["body"] for example.
I also do no know if there is a better way to handle this.
I had considered converting the keys to a 'type' using an enum, but again not knowing the enum cases ahead of time makes this impossible.
I know textEntries this does not change and I know id, text and locale this does not change. It is the keys in between this layer I do not know. I have tried the helpful solution posted by #vadian but cannot seem to make this work in the context of only needing 1 set of keys decoded.
For the proposed solution in this answer the structs are
struct ContentArticleTextEntries: Decodable {
let title : String
let id: String
let text: String
let locale: Locale
enum CodingKeys: String, CodingKey {
case id, text, locale
}
init(from decoder: Decoder) throws {
self.title = try decoder.currentTitle()
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(String.self, forKey: .id)
self.text = try container.decode(String.self, forKey: .text)
let localeIdentifier = try container.decode(String.self, forKey: .locale)
self.locale = Locale(identifier: localeIdentifier)
}
}
struct ContentArticle: TitleDecodable {
let title : String
var elements: [ContentArticleTextEntries]
}
struct Container: Decodable {
let containers: [ContentArticle]
init(from decoder: Decoder) throws {
self.containers = try decoder.decodeTitledElements(ContentArticle.self)
}
}
Then decode Container.self
If your models are like,
struct ContentArticle: Decodable {
let textEntries: [String: ContentArticleTextEntries]
}
struct ContentArticleTextEntries: Decodable {
var id: String
var text: String
var locale: String
}
Then, you can simply access the data based on key like,
let response = try JSONDecoder().decode(ContentArticle.self, from: data)
let key = "summary"
print(response.textEntries[key])
Note: No need to write enum CodingKeys and init(from:) if there is no special handling while parsing the JSON.
Use "decodeIfPresent" variant method instead of decode method also you need to breakdown the ContentArticleTextEntries dictionary into individual keys:
struct ContentArticle: Decodable {
var id: String
var text: String?
var locale: String?
private enum CodingKeys: String, CodingKey {
case id, text, locale
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decodeIfPresent(String.self, forKey: .id) ?? ""
self.text = try container.decodeIfPresent(String.self, forKey: .text)
self.locale = try container.decodeIfPresent(String.self, forKey: .locale)
}
}

How to dynamically add properties to a constant Decodable object in Swift?

Background
Basically I got an api that returns something like this:
"order_detail": [
{
"id": 6938,
"order_id": 6404,
"item_price": "4",
..
"item": {
"id": 12644,
"ref": "Iced Caffe Americano",
"short_description": "",
..
and in my decodable obj i got this
public struct OrderDetail: Decodable {
public let id: Int
public let order_id: Int
public let item_price: String?
..
public let item: Item?
and
public struct Item: Decodable {
public var id: Int
public var ref: String?
public var short_description: String?
The problem is that somewhere else in the code, there is a method that's expecting the Item object to have item_price.
Question
What I want to do is swizzle or mutate this constant Item object and dynamically add item_price property to it.. How can I do that?
Workarounds, other solutions
1. Change json
I know there are many other solutions to this same problem (I'm working on it as we speak, which is simply modifying the api endpoint to suit my needs).. but again that option is not always possible (ie suppose the backend team is separate)
2. Change the function expectation
That is also possible, but also not inexpensive as this function is used in many other places in the app which I don't potentially have control over
If you want to add a property to a Decodable type that's not part of its JSON representation, so simply need to declare a CodingKey conformant type and leave out the specific property name so that the automatically synthesised init(from decoder:Decoder) initialiser will know not to look for that value in the JSON.
Btw you should also conform to the Swift naming convention (lowerCamelCase for variable names) and use CodingKey to map the JSON keys to the property names.
public struct Item: Decodable {
public var id: Int
public var ref: String?
public var shortDescription: String?
public var itemPrice: String? // or whatever else its type needs to be
private enum CodingKeys: String, CodingKey {
case id, ref, shortDescription = "short_description"
}
}
This is one way to achieve this
Take over the initialization of Item in OrderDetail decoding.
struct OrderDetail: Decodable {
let id: Int
let orderId: Int
let itemPrice: String?
let item: Item
private enum OrderDetailCodingKey: CodingKey {
case id
case orderId
case itemPrice
case item
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: OrderDetailCodingKey.self)
self.id = try container.decode(Int.self, forKey: .id)
self.orderId = try container.decode(Int.self, forKey: .orderId)
let itemPrice = try container.decode(String?.self, forKey: .itemPrice)
self.itemPrice = itemPrice
self.item = try Item(from: decoder, itemPrice: itemPrice)
}
}
Use a custom initializer to create your item.
struct Item: Decodable {
let id: Int
let ref: String?
let shortDescription: String?
let itemPrice: String?
private enum ItemCodingKeys: CodingKey {
case id
case ref
case shortDescription
}
init(from decoder: Decoder, itemPrice: String?) throws {
let container = try decoder.container(keyedBy: ItemCodingKeys.self)
self.id = try container.decode(Int.self, forKey: .id)
self.ref = try? container.decode(String.self, forKey: .ref)
self.shortDescription = try? container.decode(String.self, forKey: .shortDescription)
self.itemPrice = itemPrice
}
}
You can call the following function to test the functionality:
private func test() {
let json = """
{"id":6938,"orderId":6404,"itemPrice":"4","item":{"id":12644,"ref":"Iced Caffe Americano","shortDescription":""}}
"""
let data = json.data(using: .utf8)
let decoder = JSONDecoder()
if let data = data {
do {
let order = try decoder.decode(OrderDetail.self, from: data)
print(order)
} catch let jsonError {
os_log("JSON decoding failed [%#]", String(describing: jsonError))
}
} else {
os_log("No data found")
}
}

Swift 4 Decodable multiple containers

I'm trying to understand how could I parse this multiple container JSON to an object. I've tried this approach (Mark answer), but he explain how to solve it using one-level container. For some reason I can't mimic the behaviour for multiple containers.
{
"graphql": {
"shortcode_media": {
"id": "1657677004214306744",
"shortcode": "BcBQHPchwe4"
}
}
}
class Post: Decodable {
enum CodingKeys: String, CodingKey {
case graphql // The top level "user" key
case shortcode_media
}
enum PostKeys: String, CodingKey {
case id
}
required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let post = try values.nestedContainer(keyedBy: PostKeys.self, forKey: .shortcode_media)
self.id = try post.decode(String.self, forKey: .id)
}
var id: String
}
I'm getting:
Swift.DecodingError.Context(codingPath: [], debugDescription: "Cannot get KeyedDecodingContainer<PostKeys> -- no value found for key \"shortcode_media\"", underlyingError: nil))
Any help will be much appreciated, thank you!
As vadian notes, you haven't matched the JSON structure. There is no shortcode_media key at the top level like you've encoded in CodingKeys.
In order to decode this with a custom decoder, you will need to walk through each level and deal with it.
class Post: Decodable {
enum CodingKeys: String, CodingKey {
case graphql
}
enum GraphQLKeys: String, CodingKey {
case shortcode_media
}
enum PostKeys: String, CodingKey {
case id
}
required init(from decoder: Decoder) throws {
// unload the top level
let container = try decoder.container(keyedBy: CodingKeys.self)
// Unload the graphql key
let graphql = try container.nestedContainer(keyedBy: GraphQLKeys.self, forKey: .graphql)
// unload the shortcode_media key
let post = try graphql.nestedContainer(keyedBy: PostKeys.self, forKey: .shortcode_media)
// Finally, unload the actual object
self.id = try post.decode(String.self, forKey: .id)
}
var id: String
}
Please read the JSON.
Any opening { is quasi a separator. The indentation of the JSON indicates also the hierarchy.
For clarity I removed all coding keys and left the variable names – which should be camelCased – unchanged.
struct Root : Decodable {
let graphql : Graph
// to access the `Media` object declare a lazy instantiated property
lazy var media : Media = {
return graphql.shortcode_media
}()
}
struct Graph : Decodable {
let shortcode_media : Media
}
struct Media : Decodable {
let id: String
let shortcode : String
}
let jsonString = """
{
"graphql": {
"shortcode_media": {
"id": "1657677004214306744",
"shortcode": "BcBQHPchwe4"
}
}
}
"""
do {
let data = Data(jsonString.utf8)
var result = try decoder.decode(Root.self, from: data)
print(result.media)
} catch {
print("error: ", error)
}
Writing a custom initializer with nestedContainer is more effort than creating the actual hierarchy.
Please paste the entire code in a Playground and check it out.

How can I implement polymorphic decoding of JSON data in Swift 4?

I am attempting to render a view from data returned from an API endpoint. My JSON looks (roughly) like this:
{
"sections": [
{
"title": "Featured",
"section_layout_type": "featured_panels",
"section_items": [
{
"item_type": "foo",
"id": 3,
"title": "Bisbee1",
"audio_url": "http://example.com/foo1.mp3",
"feature_image_url" : "http://example.com/feature1.jpg"
},
{
"item_type": "bar",
"id": 4,
"title": "Mortar8",
"video_url": "http://example.com/video.mp4",
"director" : "John Smith",
"feature_image_url" : "http://example.com/feature2.jpg"
}
]
}
]
}
I have an object that represents how to layout a view in my UI. It looks like this:
public struct ViewLayoutSection : Codable {
var title: String = ""
var sectionLayoutType: String
var sectionItems: [ViewLayoutSectionItemable] = []
}
ViewLayoutSectionItemable is a protocol that includes, among other things, a title and a URL to an image to use in the layout.
However, the sectionItems array is actually made up of different types. What I'd like to do is instantiate each section item as an instance of its own class.
How do I setup the init(from decoder: Decoder) method for the ViewLayoutSection to let me iterate over the items in that JSON array and create an instance of the proper class in each case?
Polymorphic design is a good thing: many design patterns exhibit polymorphism to make the overall system more flexible and extensible.
Unfortunately, Codable doesn't have "built in" support for polymorphism, at least not yet.... there's also discussion about whether this is actually a feature or a bug.
Fortunately, you can pretty easily create polymorphic objects using an enum as an intermediate "wrapper."
First, I'd recommend declaring itemType as a static property, instead of an instance property, to make switching on it easier later. Thereby, your protocol and polymorphic types would look like this:
import Foundation
public protocol ViewLayoutSectionItemable: Decodable {
static var itemType: String { get }
var id: Int { get }
var title: String { get set }
var imageURL: URL { get set }
}
public struct Foo: ViewLayoutSectionItemable {
// ViewLayoutSectionItemable Properties
public static var itemType: String { return "foo" }
public let id: Int
public var title: String
public var imageURL: URL
// Foo Properties
public var audioURL: URL
}
public struct Bar: ViewLayoutSectionItemable {
// ViewLayoutSectionItemable Properties
public static var itemType: String { return "bar" }
public let id: Int
public var title: String
public var imageURL: URL
// Bar Properties
public var director: String
public var videoURL: URL
}
Next, create an enum for the "wrapper":
public enum ItemableWrapper: Decodable {
// 1. Keys
fileprivate enum Keys: String, CodingKey {
case itemType = "item_type"
case sections
case sectionItems = "section_items"
}
// 2. Cases
case foo(Foo)
case bar(Bar)
// 3. Computed Properties
public var item: ViewLayoutSectionItemable {
switch self {
case .foo(let item): return item
case .bar(let item): return item
}
}
// 4. Static Methods
public static func items(from decoder: Decoder) -> [ViewLayoutSectionItemable] {
guard let container = try? decoder.container(keyedBy: Keys.self),
var sectionItems = try? container.nestedUnkeyedContainer(forKey: .sectionItems) else {
return []
}
var items: [ViewLayoutSectionItemable] = []
while !sectionItems.isAtEnd {
guard let wrapper = try? sectionItems.decode(ItemableWrapper.self) else { continue }
items.append(wrapper.item)
}
return items
}
// 5. Decodable
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Keys.self)
let itemType = try container.decode(String.self, forKey: Keys.itemType)
switch itemType {
case Foo.itemType: self = .foo(try Foo(from: decoder))
case Bar.itemType: self = .bar(try Bar(from: decoder))
default:
throw DecodingError.dataCorruptedError(forKey: .itemType,
in: container,
debugDescription: "Unhandled item type: \(itemType)")
}
}
}
Here's what the above does:
You declare Keys that are relevant to the response's structure. In your given API, you're interested in sections and sectionItems. You also need to know which key represents the type, which you declare here as itemType.
You then explicitly list every possible case: this violates the Open Closed Principle, but this is "okay" to do as it's acting as a "factory" for creating items....
Essentially, you'll only have this ONCE throughout your entire app, just right here.
You declare a computed property for item: this way, you can unwrap the underlying ViewLayoutSectionItemable without needing to care about the actual case.
This is the heart of the "wrapper" factory: you declare items(from:) as a static method that's capable of returning [ViewLayoutSectionItemable], which is exactly what you want to do: pass in a Decoder and get back an array containing polymorphic types! This is the method you'll actually use instead of decoding Foo, Bar or any other polymorphic arrays of these types directly.
Lastly, you must make ItemableWrapper implement the Decodable method. The trick here is that ItemWrapper always decodes an ItemWrapper: thereby, this works how Decodable is expecting.
As it's an enum, however, it's allowed to have associated types, which is exactly what you do for each case. Hence, you can indirectly create polymorphic types!
Since you've done all the heavy lifting within ItemWrapper, it's very easy to now go from a Decoder to an `[ViewLayoutSectionItemable], which you'd do simply like this:
let decoder = ... // however you created it
let items = ItemableWrapper.items(from: decoder)
A simpler version of #CodeDifferent's response, which addresses #JRG-Developer's comment. There is no need to rethink your JSON API; this is a common scenario. For each new ViewLayoutSectionItem you create, you only need to add one case and one line of code to the PartiallyDecodedItem.ItemKind enum and PartiallyDecodedItem.init(from:) method respectively.
This is not only the least amount of code compared to the accepted answer, it is more performant. In #CodeDifferent's option, you are required to initialize 2 arrays with 2 different representations of the data to get your array of ViewLayoutSectionItems. In this option, you still need to initialize 2 arrays, but get to only have one representation of the data by taking advantage of copy-on-write semantics.
Also note that it is not necessary to include ItemType in the protocol or the adopting structs (it doesn't make sense to include a string describing what type a type is in a statically typed language).
protocol ViewLayoutSectionItem {
var id: Int { get }
var title: String { get }
var imageURL: URL { get }
}
struct Foo: ViewLayoutSectionItem {
let id: Int
let title: String
let imageURL: URL
let audioURL: URL
}
struct Bar: ViewLayoutSectionItem {
let id: Int
let title: String
let imageURL: URL
let videoURL: URL
let director: String
}
private struct PartiallyDecodedItem: Decodable {
enum ItemKind: String, Decodable {
case foo, bar
}
let kind: Kind
let item: ViewLayoutSectionItem
private enum DecodingKeys: String, CodingKey {
case kind = "itemType"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: DecodingKeys.self)
self.kind = try container.decode(Kind.self, forKey: .kind)
self.item = try {
switch kind {
case .foo: return try Foo(from: decoder)
case .number: return try Bar(from: decoder)
}()
}
}
struct ViewLayoutSection: Decodable {
let title: String
let sectionLayoutType: String
let sectionItems: [ViewLayoutSectionItem]
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.title = try container.decode(String.self, forKey: .title)
self.sectionLayoutType = try container.decode(String.self, forKey: .sectionLayoutType)
self.sectionItems = try container.decode([PartiallyDecodedItem].self, forKey: .sectionItems)
.map { $0.item }
}
}
To handle the snake case -> camel case conversion, rather than manually type out all of the keys, you can simply set a property on JSONDecoder
struct Sections: Decodable {
let sections: [ViewLayoutSection]
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let sections = try decode(Sections.self, from: json)
.sections
I recommend you to be judicious on the use of Codable. If you only want to decode a type from JSON and not encode it, conforming it to Decodable alone is enough. And since you have already discovered that you need to decode it manually (via a custom implementation of init(from decoder: Decoder)), the question becomes: what is the least painful way to do it?
First, the data model. Note that ViewLayoutSectionItemable and its adopters do not conform to Decodable:
enum ItemType: String, Decodable {
case foo
case bar
}
protocol ViewLayoutSectionItemable {
var id: Int { get }
var itemType: ItemType { get }
var title: String { get set }
var imageURL: URL { get set }
}
struct Foo: ViewLayoutSectionItemable {
let id: Int
let itemType: ItemType
var title: String
var imageURL: URL
// Custom properties of Foo
var audioURL: URL
}
struct Bar: ViewLayoutSectionItemable {
let id: Int
let itemType: ItemType
var title: String
var imageURL: URL
// Custom properties of Bar
var videoURL: URL
var director: String
}
Next, here's how we will decode the JSON:
struct Sections: Decodable {
var sections: [ViewLayoutSection]
}
struct ViewLayoutSection: Decodable {
var title: String = ""
var sectionLayoutType: String
var sectionItems: [ViewLayoutSectionItemable] = []
// This struct use snake_case to match the JSON so we don't have to provide a custom
// CodingKeys enum. And since it's private, outside code will never see it
private struct GenericItem: Decodable {
let id: Int
let item_type: ItemType
var title: String
var feature_image_url: URL
// Custom properties of all possible types. Note that they are all optionals
var audio_url: URL?
var video_url: URL?
var director: String?
}
private enum CodingKeys: String, CodingKey {
case title
case sectionLayoutType = "section_layout_type"
case sectionItems = "section_items"
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
title = try container.decode(String.self, forKey: .title)
sectionLayoutType = try container.decode(String.self, forKey: .sectionLayoutType)
sectionItems = try container.decode([GenericItem].self, forKey: .sectionItems).map { item in
switch item.item_type {
case .foo:
// It's OK to force unwrap here because we already
// know what type the item object is
return Foo(id: item.id, itemType: item.item_type, title: item.title, imageURL: item.feature_image_url, audioURL: item.audio_url!)
case .bar:
return Bar(id: item.id, itemType: item.item_type, title: item.title, imageURL: item.feature_image_url, videoURL: item.video_url!, director: item.director!)
}
}
}
Usage:
let sections = try JSONDecoder().decode(Sections.self, from: json).sections
I have written a blog post about this exact problem.
In summary. I suggest defining an extension on Decoder
extension Decoder {
func decode<ExpectedType>(_ expectedType: ExpectedType.Type) throws -> ExpectedType {
let container = try self.container(keyedBy: PolymorphicMetaContainerKeys.self)
let typeID = try container.decode(String.self, forKey: .itemType)
guard let types = self.userInfo[.polymorphicTypes] as? [Polymorphic.Type] else {
throw PolymorphicCodableError.missingPolymorphicTypes
}
let matchingType = types.first { type in
type.id == typeID
}
guard let matchingType = matchingType else {
throw PolymorphicCodableError.unableToFindPolymorphicType(typeID)
}
let decoded = try matchingType.init(from: self)
guard let decoded = decoded as? ExpectedType else {
throw PolymorphicCodableError.unableToCast(
decoded: decoded,
into: String(describing: ExpectedType.self)
)
}
return decoded
}
}
Then adding the possible polymorphic types to the Decoder instance:
var decoder = JSONDecoder()
decoder.userInfo[.polymorphicTypes] = [
Snake.self,
Dog.self
]
If you have nested polymeric values you can write a property wrapper to that calls this decode method so that you do not need to define custom init(from:).
Here's a small utility package that resolve this exact problem.
It was built around a configuration type that has variants for the decodable type defines the type information discriminator.
enum DrinkFamily: String, ClassFamily {
case drink = "drink"
case beer = "beer"
static var discriminator: Discriminator = .type
typealias BaseType = Drink
func getType() -> Drink.Type {
switch self {
case .beer:
return Beer.self
case .drink:
return Drink.self
}
}
}
Later in your collection overload the init method to use our KeyedDecodingContainer extension.
class Bar: Decodable {
let drinks: [Drink]
private enum CodingKeys: String, CodingKey {
case drinks
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
drinks = try container.decodeHeterogeneousArray(OfFamily: DrinkFamily.self, forKey: .drinks)
}
}