Handling JSON Array Containing Multiple Types - Swift 4 Decodable
Asked Answered
B

3

8

I am trying to use Swift 4 Decodable to parse an array that contains two different types of objects. The data looks something like this, with the included array being the one that contains both Member and ImageMedium objects:

{
  "data": [{
    "id": "8f7cbbac-c133-4b5e-a2ec-1f32353018fa",
    "type": "post",
    "title": "Test Post 1",
    "owner-id": "8986563c-438c-4d77-8115-9e5de2b6e477",
    "owner-type": "member"
  }, {
    "id": "f6b3c640-a58b-449f-93c7-f6cb7b569a9c",
    "type": "post",
    "title": "Test Post 2",
    "owner-id": "38d845a4-db66-48b9-9c15-d857166e255e",
    "owner-type": "member"
  }],
  "included": [{
    "id": "8986563c-438c-4d77-8115-9e5de2b6e477",
    "type": "member",
    "first-name": "John",
    "last-name": "Smith"
  }, {
    "id": "d7218ca1-de53-4832-bb8f-dbceb6747e98",
    "type": "image-medium",
    "asset-url": "https://faketest.com/fake-test-1.png",
    "owner-id": "f6b3c640-a58b-449f-93c7-f6cb7b569a9c",
    "owner-type": "post"
  }, {
    "id": "c59b8c72-13fc-44fd-8ef9-4b0f8fa486a0",
    "type": "image-medium",
    "asset-url": "https://faketest.com/fake-test-2.png",
    "owner-id": "8f7cbbac-c133-4b5e-a2ec-1f32353018fa",
    "owner-type": "post"
  }, {
    "id": "38d845a4-db66-48b9-9c15-d857166e255e",
    "type": "member",
    "first-name": "Jack",
    "last-name": "Doe"
  }]
}

I have tried a bunch of different ways to solve this cleanly using Decodable, but so far the only thing that has worked for me is making one struct for Included that contains all the properties of both objects as optionals, like this:

struct Root: Decodable {
    let data: [Post]?
    let included: [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 Included: Decodable {
    let id: String?
    let type: String?
    let assetUrl: String?
    let ownerId: String?
    let ownerType: String?
    let firstName: String?
    let lastName: String?

    enum CodingKeys: String, CodingKey {
        case id
        case type
        case assetUrl = "asset-url"
        case ownerId = "owner-id"
        case ownerType = "owner-type"
        case firstName = "first-name"
        case lastName = "last-name"
    }
} 

This can work by implementing a method to create Member and ImageMedium objects from the Included struct based off what its type property is, however it's obviously less than ideal. I'm hoping there is a way to accomplish this using a custom init(from decoder: Decoder), but I haven't gotten it to work yet. Any ideas?

Buchholz answered 28/3, 2018 at 15:28 Comment(3)
You should handle it with Codable enum with associated values of those types, of course.Flout
@user28434 could you elaborate? I edited the question to included the Root and Post structs if that helps.Buchholz
link: #48315224Train
B
16

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"
    }
}
Buchholz answered 28/3, 2018 at 21:20 Comment(1)
The solution works. But it discards the ordering info of elements decoded from included.Cuellar
B
2

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: ).

Bernete answered 28/3, 2018 at 16:8 Comment(0)
U
1

My suggestion is to use a single type Post for all items. To distinguish the different types decode the type key as enum and decode the properties depending on the case.

That requires to declare all non-global properties as var.

struct Root : Decodable {
    let data : [Post]
    let included : [Post]
}

enum PostType : String, Decodable {
    case member, post, imageMedium = "image-medium"
}

struct Post : Decodable {
    let id: String
    let type: PostType

    var title: String?
    var assetUrl: String?
    var ownerId: String?
    var ownerType: String?
    var firstName: String?
    var lastName: String?

    enum CodingKeys: String, CodingKey {
        case id, type, 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)
        id = try container.decode(String.self, forKey: .id)
        type = try container.decode(PostType.self, forKey: .type)
        switch type {
        case .member:
            firstName = try container.decode(String.self, forKey: .firstName)
            lastName = try container.decode(String.self, forKey: .lastName)
        case .post:
            title = try container.decode(String.self, forKey: .title)
            ownerId = try container.decode(String.self, forKey: .ownerId)
            ownerType = try container.decode(String.self, forKey: .ownerType)
        case .imageMedium:
            assetUrl = try container.decode(String.self, forKey: .assetUrl)
            ownerId = try container.decode(String.self, forKey: .ownerId)
            ownerType = try container.decode(String.self, forKey: .ownerType)
        }
    }
} 
Ululant answered 28/3, 2018 at 15:59 Comment(1)
That requires to declare all non-global properties as var. — it really doesn't. It requires to declare the Optional, but it can be let, but you should manually initialize them with nil inside of init.Flout

© 2022 - 2025 — McMap. All rights reserved.