Swift JSON-декодирование с использованием codable вместо этого обнаружило строку / данные - PullRequest
2 голосов
/ 01 мая 2019

Я играю с API New York Times и получаю сообщение

typeMismatch (Swift.Array, Swift.DecodingError.Context (codingPath: [CodingKeys (stringValue: "results",intValue: nil), _JSONKey (stringValue: «Index 3», intValue: 3), CodingKeys (stringValue: «multimedia», intValue: nil)], debugDescription: «Ожидается декодирование массива, но вместо этого найдена строка / данные.»,underError: nil))

Это происходит, когда мультимедийная часть JSON представляет собой строку, а не массив: аналогично этому вопросу SO:

Ожидается, что Swift Codableрасшифровать словарьно вместо этого нашел строку / данные

Поэтому я решил сделать минимальный пример.

Со статьей

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"
    }
}

и мультимедиа

struct Multimedia: Codable {
    var url: String?

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

Я могу использовать строку JSON

        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"
        }
      ]
    }
"""

с кодом:

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

Однако следующая строка JSON декодируется как 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": ""
        }
"""

Несмотря на то, что я сделал свойства в своих объектах необязательными, они использовали .self в методе decoder.decode.

Как мне получить вторую строку JSON для декодирования?

Ответы [ 3 ]

1 голос
/ 01 мая 2019

Ну, этот вид несоответствия должен быть обработан API. Но вы можете изящно обрабатывать различные типы возвращаемых данных, введя enum, как показано ниже,

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?
}
1 голос
/ 01 мая 2019

Решение состоит в том, чтобы декодировать все ключи вручную , значения которых могут быть разных типов.

В этом примере multimedia является необязательным ([Multimedia] или nil), а perFacet не является обязательным [String], который является пустым, если значение является пустой строкой.

Все члены структуры являются константами (let), и добавлена ​​стратегия convertFromSnakeCase, чтобы избавиться от 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) }
1 голос
/ 01 мая 2019

РЕШЕНИЕ

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)
}

РЕДАКТИРОВАТЬ

Попробуйте это

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
}

Ссылка: https://medium.com/swiftly-swift/swift-4-decodable-beyond-the-basics-990cc48b7375

...