Handling JSON Array Containing Multiple Types - Swift 4 Decodable
I figured out how to decode the mixed included
array into two arrays of one type each. Using two Decodable structs is easier to deal with, and more versatile, than having one struct to cover multiple types of data.
This is what my final solution looks like for anyone who's interested:
struct Root: Decodable {
let data: [Post]?
let members: [Member]
let images: [ImageMedium]
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
data = try container.decode([Post].self, forKey: .data)
var includedArray = try container.nestedUnkeyedContainer(forKey: .included)
var membersArray: [Member] = []
var imagesArray: [ImageMedium] = []
while !includedArray.isAtEnd {
do {
if let member = try? includedArray.decode(Member.self) {
membersArray.append(member)
}
else if let image = try? includedArray.decode(ImageMedium.self) {
imagesArray.append(image)
}
}
}
members = membersArray
images = imagesArray
}
enum CodingKeys: String, CodingKey {
case data
case included
}
}
struct Post: Decodable {
let id: String?
let type: String?
let title: String?
let ownerId: String?
let ownerType: String?
enum CodingKeys: String, CodingKey {
case id
case type
case title
case ownerId = "owner-id"
case ownerType = "owner-type"
}
}
struct Member: Decodable {
let id: String?
let type: String?
let firstName: String?
let lastName: String?
enum CodingKeys: String, CodingKey {
case id
case type
case firstName = "first-name"
case lastName = "last-name"
}
}
struct ImageMedium: Decodable {
let id: String?
let type: String?
let assetUrl: String?
let ownerId: String?
let ownerType: String?
enum CodingKeys: String, CodingKey {
case id
case type
case assetUrl = "asset-url"
case ownerId = "owner-id"
case ownerType = "owner-type"
}
}
This is based on initial edit and it has some redundant code, but general idea should be understandable:
enum Post: Codable {
case post(id: UUID, title: String, ownerId: UUID, ownerType: PostOwner)
case member(id: UUID, firstName: String, lastName: String)
case imageMedium(id: UUID, assetURL: URL, ownerId: UUID, ownerType: ImageOwner)
enum PostType: String, Codable {
case post
case member
case imageMedium = "image-medium"
}
enum PostOwner: String, Codable {
case member
}
enum ImageOwner: String, Codable {
case post
}
enum CodingKeys: String, CodingKey {
case id
case type
case title
case assetUrl = "asset-url"
case ownerId = "owner-id"
case ownerType = "owner-type"
case firstName = "first-name"
case lastName = "last-name"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let id = try container.decode(UUID.self, forKey: .id)
let type = try container.decode(PostType.self, forKey: .type)
switch type {
case .post:
let title = try container.decode(String.self, forKey: .title)
let ownerId = try container.decode(UUID.self, forKey: .ownerId)
let ownerType = try container.decode(PostOwner.self, forKey: .ownerType)
self = .post(id: id, title: title, ownerId: ownerId, ownerType: ownerType)
case .member:
let firstName = try container.decode(String.self, forKey: .firstName)
let lastName = try container.decode(String.self, forKey: .lastName)
self = .member(id: id, firstName: firstName, lastName: lastName)
case .imageMedium:
let assetURL = try container.decode(URL.self, forKey: .assetUrl)
let ownerId = try container.decode(UUID.self, forKey: .ownerId)
let ownerType = try container.decode(ImageOwner.self, forKey: .ownerType)
self = .imageMedium(id: id, assetURL: assetURL, ownerId: ownerId, ownerType: ownerType)
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .post(let id, let title, let ownerId, let ownerType):
try container.encode(PostType.post, forKey: .type)
try container.encode(id, forKey: .id)
try container.encode(title, forKey: .title)
try container.encode(ownerId, forKey: .ownerId)
try container.encode(ownerType, forKey: .ownerType)
case .member(let id, let firstName, let lastName):
try container.encode(PostType.member, forKey: .type)
try container.encode(id, forKey: .id)
try container.encode(firstName, forKey: .firstName)
try container.encode(lastName, forKey: .lastName)
case .imageMedium(let id, let assetURL, let ownerId, let ownerType):
try container.encode(PostType.imageMedium, forKey: .type)
try container.encode(id, forKey: .id)
try container.encode(assetURL, forKey: .assetUrl)
try container.encode(ownerId, forKey: .ownerId)
try container.encode(ownerType, forKey: .ownerType)
}
}
}
let jsonDecoder = JSONDecoder()
let result = try jsonDecoder.decode([String: [Post]].self, from: yourJSONData)
print(result)
It has zero optionals for fields not used in the current post type, and UUID
s are typed as UUID
, and URL
s as URL
instead of String
s everywhere.
ownerType
are typed as PostOwner
and ImageOwner
for .post
and .imageMedium
for extra type safety.
EDIT: Ok, i checked edit of the question: In your json only ".post"s go into "data", and rest goes into "included". In mine answer Post
s and Included
s are merged into one single type.
So it should be like this:
struct Post: Codable {
let id: UUID
let title: String
let ownerId: UUID
let ownerType: PostOwner
enum PostOwner: String, Codable {
case member
}
enum CodingKeys: String, CodingKey {
case id
case title
case ownerId = "owner-id"
case ownerType = "owner-type"
}
}
enum Included: Codable {
case member(id: UUID, firstName: String, lastName: String)
case imageMedium(id: UUID, assetURL: URL, ownerId: UUID, ownerType: ImageOwner)
enum PostType: String, Codable {
case member
case imageMedium = "image-medium"
}
enum ImageOwner: String, Codable {
case post
}
enum CodingKeys: String, CodingKey {
case id
case type
case title
case assetUrl = "asset-url"
case ownerId = "owner-id"
case ownerType = "owner-type"
case firstName = "first-name"
case lastName = "last-name"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let id = try container.decode(UUID.self, forKey: .id)
let type = try container.decode(PostType.self, forKey: .type)
switch type {
case .member:
let firstName = try container.decode(String.self, forKey: .firstName)
let lastName = try container.decode(String.self, forKey: .lastName)
self = .member(id: id, firstName: firstName, lastName: lastName)
case .imageMedium:
let assetURL = try container.decode(URL.self, forKey: .assetUrl)
let ownerId = try container.decode(UUID.self, forKey: .ownerId)
let ownerType = try container.decode(ImageOwner.self, forKey: .ownerType)
self = .imageMedium(id: id, assetURL: assetURL, ownerId: ownerId, ownerType: ownerType)
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .member(let id, let firstName, let lastName):
try container.encode(PostType.member, forKey: .type)
try container.encode(id, forKey: .id)
try container.encode(firstName, forKey: .firstName)
try container.encode(lastName, forKey: .lastName)
case .imageMedium(let id, let assetURL, let ownerId, let ownerType):
try container.encode(PostType.imageMedium, forKey: .type)
try container.encode(id, forKey: .id)
try container.encode(assetURL, forKey: .assetUrl)
try container.encode(ownerId, forKey: .ownerId)
try container.encode(ownerType, forKey: .ownerType)
}
}
}
Post
type parsing/validating could/should be added by manually coding init(from: )
.