3

I'm playing with the New York Times API and am getting the message

typeMismatch(Swift.Array, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "results", intValue: nil), _JSONKey(stringValue: "Index 3", intValue: 3), CodingKeys(stringValue: "multimedia", intValue: nil)], debugDescription: "Expected to decode Array but found a string/data instead.", underlyingError: nil))

This happens when the multimedia part of the JSON is a string rather than an array: similar to this SO question:

Swift Codable expected to decode Dictionary<String, Any>but found a string/data instead

So I decided to make a minimum example.

With article

public struct Article : Codable {
    var abstract: String?
    var thumbnail_standard: String?
    var multimedia: [Multimedia]?
    var title: String?
    var url: URL?

    private enum CodingKeys: String, CodingKey {
        case abstract = "abstract"
        case multimedia = "multimedia"
        case thumbnail_standard = "thumbnail_standard"
        case title = "title"
        case url = "url"
    }
}

and multimedia

struct Multimedia: Codable {
    var url: String?

    private enum CodingKeys: String, CodingKey {
        case url = "url"
    }
}

I can use JSON string

        let jsonString = """
{
      "slug_name": "30dc-emoluments",
      "section": "U.S.",
      "subsection": "Politics",
      "title": "Congressional Democrats’ Lawsuit Examining Trump’s Private Business Can Proceed, Federal Judge Says",
      "abstract": "The decision is at least a temporary victory for the president’s critics who say he is willfully flaunting constitutional bans.",
      "url": "https://www.nytimes.com/2019/04/30/us/politics/trump-emoluments-clauses.html",
      "byline": "By SHARON LaFRANIERE",
      "thumbnail_standard": "https://static01.nyt.com/images/2019/04/19/world/30dc-emoluments/30dc-emoluments-thumbStandard.jpg",
      "item_type": "Article",
      "source": "The New York Times",
      "updated_date": "2019-04-30T22:09:45-04:00",
      "created_date": "2019-04-30T21:56:05-04:00",
      "published_date": "2019-04-29T20:00:00-04:00",
      "first_published_date": "2019-04-30T21:54:34-04:00",
      "material_type_facet": "News",
      "kicker": null,
      "subheadline": null,
      "des_facet": "",
      "org_facet": [
        "Democratic Party",
        "Constitution (US)",
        "Justice Department",
        "Trump International Hotel (Washington, DC)"
      ],
      "per_facet": [
        "Sullivan, Emmet G",
        "Trump, Donald J"
      ],
      "geo_facet": "",
      "related_urls": [
        {
          "suggested_link_text": "Appeals Court Judges Appear Skeptical of Emoluments Case Against Trump",
          "url": "https://www.nytimes.com/2019/03/19/us/politics/trump-emoluments-lawsuit.html"
        },
        {
          "suggested_link_text": "Democrats in Congress Sue Trump Over Foreign Business Dealings",
          "url": "https://www.nytimes.com/2017/06/14/us/politics/democrats-in-congress-to-sue-trump-over-foreign-business-dealings.html"
        }
      ],
      "multimedia": [
        {
          "url": "https://static01.nyt.com/images/2019/04/19/world/30dc-emoluments/30dc-emoluments-thumbStandard.jpg",
          "format": "Standard Thumbnail",
          "height": 75,
          "width": 75,
          "type": "image",
          "subtype": "photo",
          "caption": "The Trump International Hotel in Washington.",
          "copyright": "Gabriella Demczuk for The New York Times"
        },
        {
          "url": "https://static01.nyt.com/images/2019/04/19/world/30dc-emoluments/30dc-emoluments-articleInline.jpg",
          "format": "Normal",
          "height": 130,
          "width": 190,
          "type": "image",
          "subtype": "photo",
          "caption": "The Trump International Hotel in Washington.",
          "copyright": "Gabriella Demczuk for The New York Times"
        },
        {
          "url": "https://static01.nyt.com/images/2019/04/19/world/30dc-emoluments/30dc-emoluments-mediumThreeByTwo210.jpg",
          "format": "mediumThreeByTwo210",
          "height": 140,
          "width": 210,
          "type": "image",
          "subtype": "photo",
          "caption": "The Trump International Hotel in Washington.",
          "copyright": "Gabriella Demczuk for The New York Times"
        },
        {
          "url": "https://static01.nyt.com/images/2019/04/19/world/30dc-emoluments/30dc-emoluments-mediumThreeByTwo440.jpg",
          "format": "mediumThreeByTwo440",
          "height": 293,
          "width": 440,
          "type": "image",
          "subtype": "photo",
          "caption": "The Trump International Hotel in Washington.",
          "copyright": "Gabriella Demczuk for The New York Times"
        }
      ]
    }
"""

just fine with the code:

if let data = jsonString.data(using: .utf8)
{
    let decoder = JSONDecoder()
    let result = try? decoder.decode(Article.self, from: data)
    print(result)
}

However the following JSON string is decoded as nill:

        let jsonString = """
        {
            "slug_name": "01a3_quote-web",
            "section": "Today’s Paper",
            "subsection": "",
            "title": "Quotation of the Day: Who Killed Atlanta’s Children? Retesting Evidence After 40 Years",
            "abstract": "Quotation of the Day for Wednesday, May 1, 2019.",
            "url": "https://www.nytimes.com/2019/04/30/todayspaper/quotation-of-the-day-who-killed-atlantas-children-retesting-evidence-after-40-years.html",
            "byline": "",
            "thumbnail_standard": "",
            "item_type": "Article",
            "source": "The New York Times",
            "updated_date": "2019-04-30T21:26:36-04:00",
            "created_date": "2019-04-30T21:26:36-04:00",
            "published_date": "2019-04-29T20:00:00-04:00",
            "first_published_date": "2019-04-30T21:25:06-04:00",
            "material_type_facet": "Quote",
            "kicker": null,
            "subheadline": null,
            "des_facet": "",
            "org_facet": "",
            "per_facet": "",
            "geo_facet": "",
            "related_urls": null,
            "multimedia": ""
        }
"""

Even though I made the properties in my objects optional, used .self in the decoder.decode method.

How can I get the second JSON string to decode?

1
  • I've not used it, but one possible solution would be to use the AnyCodable library (github.com/Flight-School/AnyCodable) and define the multimedia field as AnyDecodable. This would require some additional work then to extract the multimedia data correctly (i.e. convert to [Multimedia], and if it fails then you know it was empty or ""), but the rest of the conversion should work in either scenario. Commented May 1, 2019 at 4:24

3 Answers 3

3

Well, this kind of inconsistency should be handled by the api. But you can handle different kind of return types gracefully by introducing an enum as below,

enum MultiMediaType: Codable {

    case string(String)
    case array(Array<Multimedia>)

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        do {
            self = .array(try container.decode([Multimedia].self))
        } catch DecodingError.typeMismatch {
            self = .string(try container.decode(String.self))
        }
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case .string(let value):
            try container.encode(value)
        case .array(let value):
            try container.encode(value)
        }
    }
}

public struct Article : Codable {
    var abstract: String?
    var thumbnail_standard: String?
    var multimedia: MultiMediaType
    var title: String?
    var url: URL?
}
Sign up to request clarification or add additional context in comments.

1 Comment

This is closest to the implemented solution.
1

SOLUTION

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    let abstract = try container.decode(String.self, forKey: .abstract)
    let thumbnailStandard = try container.decode(String.self, forKey: .thumbnailStandard)
    var multimedia: [Multimedia] = []
    do {
        multimedia = try container.decode([Multimedia].self, forKey: .multimedia)
    } catch {}
    let title = try container.decode(String.self, forKey: .title)
    let url = try container.decode(URL.self, forKey: .url)

    self.init(abstract: abstract, thumbnailStandard: thumbnailStandard, multimedia: multimedia, title: title, url: url)
}

EDIT

Try this

struct Article {
    let abstract: String
    let thumbnailStandard: String
    let multimedia: [Multimedia]
    let title: String
    let url: URL
}

extension Article: Decodable {
    enum CodingKeys: String, CodingKey {
        case abstract
        case thumbnailStandard = "thumbnail_standard"
        case multimedia
        case title
        case url
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let abstract = try container.decode(String.self, forKey: .abstract)
        let thumbnailStandard = try container.decode(String.self, forKey: .thumbnailStandard)
        let multimedia = try container.decode([Multimedia].self, forKey: .multimedia)
        let title = try container.decode(String.self, forKey: .title)
        let url = try container.decode(URL.self, forKey: .url)

        self.init(abstract: abstract, thumbnailStandard: thumbnailStandard, multimedia: multimedia, title: title, url: url)
    }
}

struct Multimedia: Codable {
    let url: String
}

Ref.: https://medium.com/swiftly-swift/swift-4-decodable-beyond-the-basics-990cc48b7375

3 Comments

Still returning nil
multimedia should return [] not ""
Yes, and we can write to the New York Times to complain. Or, possibly, accept that it returns "" and create a solution.
1

A solution is to decode all keys manually whose value can be different types.

In this example multimedia is optional ([Multimedia] or nil) and perFacet is non-optional [String] which is empty if the value is an empty string.

All struct members are constants (let) and the convertFromSnakeCase strategy is added to get rid of the snake_cased names

struct Article : Decodable {
    let abstract: String
    let thumbnailStandard: String
    let multimedia: [Multimedia]?
    let perFacet : [String]
    let title: String
    let url: URL

    private enum CodingKeys: String, CodingKey {
        case abstract, multimedia, thumbnailStandard, title, url, perFacet
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        abstract = try container.decode(String.self, forKey: .abstract)
        thumbnailStandard = try container.decode(String.self, forKey: .thumbnailStandard)
        do {
            perFacet = try container.decode([String].self, forKey: .perFacet)
        } catch DecodingError.typeMismatch {
            perFacet = []
        }
        do {
            multimedia = try container.decode([Multimedia].self, forKey: .multimedia)
        } catch DecodingError.typeMismatch {
            multimedia = nil
        }
        title = try container.decode(String.self, forKey: .title)
        url = try container.decode(URL.self, forKey: .url)
    }
}

struct Multimedia: Decodable {
    let url: URL
    let format, type, subtype, caption, copyright: String
    let height, width: Int
}

let data = Data(jsonString.utf8)

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
    let result = try decoder.decode(Article.self, from: data)
    print(result)
} catch { print(error) }

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.