2017-06-21 29 views
8

Swift's JSONDecoder oferuje właściwość dateDecodingStrategy, która pozwala nam zdefiniować sposób interpretowania przychodzących łańcuchów dat zgodnie z obiektem DateFormatter.JSONDecoder Swift z wieloma formatami dat w ciągu JSON?

Jednak obecnie pracuję z interfejsem API, który zwraca zarówno ciągi znaków daty (yyyy-MM-dd), jak i łańcuchy datetime (yyyy-MM-dd HH:mm:ss), w zależności od właściwości. Czy istnieje sposób na obsłużenie tego obiektu JSONDecoder, ponieważ dostarczony obiekt DateFormatter może obsłużyć tylko jeden obiekt dateFormat na raz?

Jednym z hamowanych ręcznych rozwiązań jest przepisanie załączonych modeli Decodable, aby zaakceptować ciągi znaków jako ich właściwości i udostępnić publiczne zmienne getter/setter, ale wydaje mi się, że to kiepskie rozwiązanie. jakieś pomysły?

Odpowiedz

15

Istnieje kilka sposobów radzenia sobie z tym:

  • można utworzyć DateFormatter podklasę, która pierwsze próby formatu ciąg daty i godziny, a następnie, jeśli to zawiedzie, próbuje zwykły format daty
  • You może dać strategię .customDate dekodowania którym poprosić Decoder dla singleValueContainer(), zdekodować ciąg, i przekazać ją przez co formatujących chcesz przed przekazaniem analizowany termin się
  • można utworzyć otoki wokół Date typ, który zapewnia zwyczaj init(from:) i encode(to:) który robi to (ale nie jest to nic lepszego niż strategia .custom)
  • Można używać zwykłego ciągi, jak sugerujesz
  • Można zapewnić zwyczaj init(from:) na wszystkich typach które wykorzystują te daty i próbować różnych rzeczy tam

w sumie dwie pierwsze metody są prawdopodobnie będzie najprostszym i najczystszych - będziesz trzymać domyślny zsyntetyzowany realizację Codable wszędzie bez utraty bezpieczeństwa typu.

+0

Pierwsze podejście to to, którego szukałem. Dzięki! – RamwiseMatt

+1

Z 'Codable' wydaje się dziwne, że wszystkie inne informacje mapowania json są dostarczane bezpośrednio z odpowiednich obiektów (np. Mapowanie do kluczy json za pomocą' CodingKeys'), ale formatowanie daty jest konfigurowane przez 'JSONDecoder' dla całego drzewa DTO . Po użyciu Mantle w przeszłości, ostatni z proponowanych rozwiązań wydaje się najbardziej odpowiedni, nawet jeśli oznacza to powtórzenie kodu mapowania dla innych pól, które mogłyby zostać wygenerowane automatycznie w inny sposób. – fabb

2

Nie można tego zrobić za pomocą jednego kodera. Najlepiej tutaj spersonalizować metody encode(to encoder:) i init(from decoder:) i zapewnić własne tłumaczenie dla jednej z tych wartości, pozostawiając wbudowaną strategię daty dla drugiej.

Warto w tym celu przyjrzeć się przekazaniu jednego lub więcej formatów do obiektu userInfo.

14

Spróbuj dekoder skonfigurowany podobnie do tego:

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

obliczu tego samego problemu, napisałem następujące rozszerzenia:

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

To rozszerzenie pozwala stworzyć DateDecodingStrategy dla JSONDecoder że uchwyty wiele różnych formatów dat w obrębie tego samego ciągu JSON. Rozszerzenie zawiera funkcję, która wymaga implementacji zamknięcia, które daje kod CodingKey, i od Ciebie zależy, czy podasz poprawny DateFormatter dla dostarczonego klucza.

Powiedzmy, że masz następujące JSON:

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

Poniższy Struct:

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

Następnie do dekodowania JSON, należy użyć następującego kodu:

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)