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 UUIDs are typed as UUID, and URLs as URL instead of Strings 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 Posts and Includeds 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: ).