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?
The three-step process to decode JSON data in SwiftPerform a network request to fetch the data. Feed the data you receive to a JSONDecoder instance. Map the JSON data to your model types by making them conform to the Decodable protocol.
Codable is the combined protocol of Swift's Decodable and Encodable protocols. Together they provide standard methods of decoding data for custom types and encoding data to be saved or transferred.
An object that converts between JSON and the equivalent Foundation objects.
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 ViewLayoutSectionItem
s. 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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With