Search code examples
iosjsonswiftcodable

Swift Codable - How to encode custom array


That's my JSON case

{
    "image_id" : 11101,
    "image_source_id" : 9,
    "image_author" : "",
    "image_copyright" : "",
    "image_format_list" : [
        {
            "image_format" : {
                "image_url" : "https://static.musixmatch.com/images-storage/mxmimages/1/0/1/1/1/11101_2.jpg",
                "image_format_id" : 2,
                "width" : 150,
                "height" : 150
            }
        },
        {
            "image_format" : {
                "image_url" : "https://static.musixmatch.com/images-storage/mxmimages/1/0/1/1/1/11101_16.jpg",
                "image_format_id" : 16,
                "width" : 451,
                "height" : 500
            }
        }
    ]
}

I correctly decode my custom object in two different classes: MXMImage & MXMImageFormat. But I can't figure out how to re-encode my object to rebuild the same JSON

That's my code:

struct MXMImage : Codable, Equatable {
    let imageId: Int
    let imageSourceId: Int
    let imageAuthor: String?
    let imageCopyright: String?
    let imageFormatList: [MXMImageFormat]?
    
    enum CodingKeys: String, Swift.CodingKey {
        case imageId
        case imageSourceId
        case imageAuthor
        case imageCopyright
        case imageFormatList
        
        enum ImageFormatListKey: String, CodingKey {
            case imageFormat
        }
    }
    
    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        imageId = try (container.decodeIfPresent(Int.self, forKey: .imageId) ?? 0)
        imageSourceId = try (container.decodeIfPresent(Int.self, forKey: .imageSourceId) ?? 0)
        imageAuthor = try? container.decodeIfPresent(String.self, forKey: .imageAuthor)
        imageCopyright = try? container.decodeIfPresent(String.self, forKey: .imageCopyright)
        
        var imagesFormatListContainer = try container.nestedUnkeyedContainer(forKey: .imageFormatList)
        var imagesList:[MXMImageFormat] = []
        while !imagesFormatListContainer.isAtEnd {
            let imageFormatContainer = try imagesFormatListContainer.nestedContainer(keyedBy: CodingKeys.ImageFormatListKey.self)
            let imageFormat = try? imageFormatContainer.decode(MXMImageFormat.self, forKey: .imageFormat)
            if let imageFormat = imageFormat {
                imagesList.append(imageFormat)
            }
        }
        self.imageFormatList = imagesList
    }
    
    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        
        try container.encodeIfPresent(imageId, forKey: .imageId)
        try container.encodeIfPresent(imageSourceId, forKey: .imageSourceId)
        try container.encodeIfPresent(imageAuthor, forKey: .imageAuthor)
        try container.encodeIfPresent(imageCopyright, forKey: .imageCopyright)
        
        var imageContainer = container.nestedUnkeyedContainer(forKey: .imageFormatList)
        try imageFormatList?.forEach { imgFormat in
            var nested = imageContainer.nestedContainer(keyedBy: CodingKeys.ImageFormatListKey.self)
            let data = try imgFormat.encoded()
            try nested.encode(data, forKey: .imageFormat)

        }
    }
}

In particular, I don't know how to re-indent my MXMImageFormat objects inside the key image_format and then encode the custom array. Is it possible to do that? Thanks in advance


Solution

  • Instead of nestedContainers you could decode/encode a [[String:MXMImageFormat]] array and map it

    struct MXMImage : Codable, Equatable {
        let imageId: Int
        let imageSourceId: Int
        let imageAuthor: String?
        let imageCopyright: String?
        let imageFormatList: [MXMImageFormat]?
    
        private enum CodingKeys : String, CodingKey { case imageId,  imageSourceId,  imageAuthor, imageCopyright, imageFormatList}
    
        public init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            imageId = try container.decode(Int.self, forKey: .imageId)
            imageSourceId = try container.decode(Int.self, forKey: .imageSourceId)
            imageAuthor = try container.decodeIfPresent(String.self, forKey: .imageAuthor)
            imageCopyright = try container.decodeIfPresent(String.self, forKey: .imageCopyright)
            if let imageFormatListData = try container.decodeIfPresent([[String:MXMImageFormat]].self, forKey: .imageFormatList) {
                imageFormatList = imageFormatListData.compactMap{$0["image_format"]}
            } else {
                imageFormatList = nil
            }
        }
    
        public func encode(to encoder: Encoder) throws {
            var container = encoder.container(keyedBy: CodingKeys.self)
            try container.encode(imageId, forKey: .imageId)
            try container.encode(imageSourceId, forKey: .imageSourceId)
            try container.encodeIfPresent(imageAuthor, forKey: .imageAuthor)
            try container.encodeIfPresent(imageCopyright, forKey: .imageCopyright)
            if let imageFormatListData = imageFormatList {
                try container.encode(imageFormatListData.map{["image_format":$0]}, forKey: .imageFormatList)
            }
        }
    }
    
    struct MXMImageFormat : Codable, Equatable {
        let imageUrl : URL
        let imageFormatId, width, height : Int
    }