2

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

2

2 Answers 2

2

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
}
Sign up to request clarification or add additional context in comments.

Comments

1

Assuming MXMImageFormat is like this:

struct MXMImageFormat : Codable {
    let imageUrl: String
    let imageFormatId: Int
    let width: Int
    let height: Int
}

I think you are overthinking this. You can just do:

try imageFormatList?.forEach { imgFormat in
    var nested = imageContainer.nestedContainer(keyedBy: CodingKeys.ImageFormatListKey.self)
    try nested.encode(imgFormat, forKey: .imageFormat)
}

Since encode accepts any Encodable, including imgFormat. You don't actually need to convert to a Data first (at least that's why you seem to be attempting to do).

Some test code:

let json = """
{
   "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
           }
       }
   ]
}
""".data(using: .utf8)!

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let model = try! decoder.decode(MXMImage.self, from: json)
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
let string = String(data: try! encoder.encode(model), encoding: .utf8)!
print(string) // this should be the same JSON as the one in the string literal

Also note that in encode, you don't have to use try? here:

while !imagesFormatListContainer.isAtEnd {
    let imageFormatContainer = try imagesFormatListContainer.nestedContainer(keyedBy: CodingKeys.ImageFormatListKey.self)
    // here vvvvvvv
    let imageFormat = try? imageFormatContainer.decode(MXMImageFormat.self, forKey: .imageFormat)
    if let imageFormat = imageFormat {
        imagesList.append(imageFormat)
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.