web-dev-qa-db-fra.com

Le JSONDecoder de Swift avec plusieurs formats de date dans une chaîne JSON?

La variable JSONDecoder de Swift offre une propriété dateDecodingStrategy qui permet de définir comment interpréter les chaînes de date entrantes conformément à un objet DateFormatter.

Cependant, je travaille actuellement avec une API qui renvoie à la fois les chaînes de date (yyyy-MM-dd) et les chaînes de date/heure (yyyy-MM-dd HH:mm:ss), en fonction de la propriété. Existe-t-il un moyen de permettre à JSONDecoder de gérer cela, étant donné que l'objet DateFormatter fourni ne peut traiter qu'une seule dateFormat à la fois?

Une solution simple consiste à réécrire les modèles Decodable fournis pour accepter uniquement les chaînes en tant que propriétés et à fournir des variables publiques Date getter/setter, mais cela me semble une mauvaise solution. Des pensées?

16
RamwiseMatt

Il y a plusieurs façons de gérer cela:

  • Vous pouvez créer une sous-classe DateFormatter qui commence par essayer le format de chaîne date-heure, puis en cas d'échec, tente le format de date brut
  • Vous pouvez définir une stratégie de décodage .customDate dans laquelle vous demandez à Decoder une singleValueContainer(), décodez une chaîne et transmettez-la à tout formateur de votre choix avant de transmettre la date d'analyse.
  • Vous pouvez créer un wrapper autour du type Date qui fournit une init(from:) et une encode(to:) personnalisées qui le font (mais ce n'est vraiment pas mieux qu'une stratégie .custom)
  • Vous pouvez utiliser des chaînes simples, comme vous le suggérez
  • Vous pouvez fournir une init(from:) personnalisée sur tous les types qui utilisent ces dates et tentent différentes opérations.

Dans l’ensemble, les deux premières méthodes seront probablement les plus simples et les plus propres. Vous garderez l’implémentation par défaut synthétisée de Codable partout sans sacrifier la sécurité de type.

24
Itai Ferber

Veuillez essayer un décodeur configuré de la même manière que ceci:

lazy var decoder: JSONDecoder = {
    let decoder = JSONDecoder()
    decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in
        let container = try decoder.singleValueContainer()
        let dateStr = try container.decode(String.self)
        // possible date strings: "2016-05-01",  "2016-07-04T17:37:21.119229Z", "2018-05-20T15:00:00Z"
        let len = dateStr.count
        var date: Date? = nil
        if len == 10 {
            date = dateNoTimeFormatter.date(from: dateStr)
        } else if len == 20 {
            date = isoDateFormatter.date(from: dateStr)
        } else {
            date = self.serverFullDateFormatter.date(from: dateStr)
        }
        guard let date_ = date else {
            throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode date string \(dateStr)")
        }
        print("DATE DECODER \(dateStr) to \(date_)")
        return date_
    })
    return decoder
}()
23
Leszek Zarna

Face à ce même problème, j'ai écrit l'extension suivante:

extension JSONDecoder.DateDecodingStrategy {
    static func custom(_ formatterForKey: @escaping (CodingKey) throws -> DateFormatter?) -> JSONDecoder.DateDecodingStrategy {
        return .custom({ (decoder) -> Date in
            guard let codingKey = decoder.codingPath.last else {
                throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "No Coding Path Found"))
            }

            guard let container = try? decoder.singleValueContainer(),
                let text = try? container.decode(String.self) else {
                    throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Could not decode date text"))
            }

            guard let dateFormatter = try formatterForKey(codingKey) else {
                throw DecodingError.dataCorruptedError(in: container, debugDescription: "No date formatter for date text")
            }

            if let date = dateFormatter.date(from: text) {
                return date
            } else {
                throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode date string \(text)")
            }
        })
    }
}

Cette extension vous permet de créer une DateDecodingStrategy pour JSONDecoder qui gère plusieurs formats de date différents dans la même chaîne JSON. L'extension contient une fonction nécessitant l'implémentation d'une fermeture qui vous donne une Clé de code, et il vous appartient de fournir le DateFormatter correct pour la clé fournie.

Disons que vous avez le JSON suivant:

{
    "publication_date": "2017-11-02",
    "opening_date": "2017-11-03",
    "date_updated": "2017-11-08 17:45:14"
}

La structure suivante:

struct ResponseDate: Codable {
    var publicationDate: Date
    var openingDate: Date?
    var dateUpdated: Date

    enum CodingKeys: String, CodingKey {
        case publicationDate = "publication_date"
        case openingDate = "opening_date"
        case dateUpdated = "date_updated"
    }
}

Ensuite, pour décoder le JSON, utilisez le code suivant:

let dateFormatterWithTime: DateFormatter = {
    let formatter = DateFormatter()

    formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"

    return formatter
}()

let dateFormatterWithoutTime: DateFormatter = {
    let formatter = DateFormatter()

    formatter.dateFormat = "yyyy-MM-dd"

    return formatter
}()

let decoder = JSONDecoder()

decoder.dateDecodingStrategy = .custom({ (key) -> DateFormatter? in
    switch key {
    case ResponseDate.CodingKeys.publicationDate, ResponseDate.CodingKeys.openingDate:
        return dateFormatterWithoutTime
    default:
        return dateFormatterWithTime
    }
})

let results = try? decoder.decode(ResponseDate.self, from: data)
10
S.Moore

essaye ça. (Swift 4)

var decoder: JSONDecoder {
    let decoder = JSONDecoder()
    decoder.dateDecodingStrategy = .custom { decoder in
        let container = try decoder.singleValueContainer()
        let dateString = try container.decode(String.self)

        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
        if let date = formatter.date(from: dateString) {
            return date
        }
        formatter.dateFormat = "yyyy-MM-dd"
        if let date = formatter.date(from: dateString) {
            return date
        }
        throw DecodingError.dataCorruptedError(in: container,
            debugDescription: "Cannot decode date string \(dateString)")
    }
    return decoder
}
4
Brownsoo Han

Il n'y a aucun moyen de faire cela avec un seul encodeur. Dans ce cas, votre meilleur choix est de personnaliser les méthodes encode(to encoder:) et init(from decoder:) et de fournir votre propre traduction pour l’une de ces valeurs, en laissant la stratégie de date intégrée pour l’autre.

Il peut être intéressant d’examiner la possibilité de passer un ou plusieurs formateurs à l’objet userInfo à cette fin.

2
Ben Scheirman

Si vous avez plusieurs dates avec différents formats dans un même modèle, il est difficile d'appliquer .dateDecodingStrategy pour chaque date.

Consultez ici https://Gist.github.com/romanroibu/089ec641757604bf78a390654c437cb0 pour une solution pratique

0
Johnykutty

Il s’agit d’une approche quelque peu verbeuse, mais plus souple: encapsulez la date avec une autre classe Date et implémentez des méthodes de sérialisation personnalisées. Par exemple:

let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"

class MyCustomDate: Codable {
    var date: Date

    required init?(_ date: Date?) {
        if let date = date {
            self.date = date
        } else {
            return nil
        }
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        let string = dateFormatter.string(from: date)
        try container.encode(string)
    }

    required public init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let raw = try container.decode(String.self)
        if let date = dateFormatter.date(from: raw) {
            self.date = date
        } else {
            throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot parse date")
        }
    }
}

Alors maintenant, vous êtes indépendant de .dateDecodingStrategy et .dateEncodingStrategy et vos dates MyCustomDate seront analysées avec le format spécifié. Utilisez-le en classe:

class User: Codable {
    var dob: MyCustomDate
}

Instancier avec

user.dob = MyCustomDate(date)
0
comm1x