文字っぽいの。

文字を書いています。写真も混ざります。

Swift OpenAPI GeneratorでISO8601拡張形式のDateが処理できないことがある

問題

Apple公式のOpenAPI Generatorを使うと、Responseをパーズできなくて下記のエラーが出ることがある。

Client error - cause description: 'Unknown', underlying error: DecodingError: dataCorrupted - at : Expected date string to be ISO8601-formatted.

原因

正確にはSwift OpenAPI Generatorではなくswift-openapi-runtimeがデフォルトで利用するISO8601DateTranscoderの実装に起因している。

public struct ISO8601DateTranscoder: DateTranscoder {

    /// Creates and returns an ISO 8601 formatted string representation of the specified date.
    public func encode(_ date: Date) throws -> String { ISO8601DateFormatter().string(from: date) }

    /// Creates and returns a date object from the specified ISO 8601 formatted string representation.
    public func decode(_ dateString: String) throws -> Date {
        guard let date = ISO8601DateFormatter().date(from: dateString) else {
            throw DecodingError.dataCorrupted(
                .init(codingPath: [], debugDescription: "Expected date string to be ISO8601-formatted.")
            )
        }
        return date
    }
}

https://github.com/apple/swift-openapi-runtime/blob/d50b48957ccb388fb89db98a56c2337276298e79/Sources/OpenAPIRuntime/Conversion/Configuration.swift#L28-L42

SwiftのISO8601DateFormatterを利用しているので良いように見えるが、ISO8601には拡張形式がありこのコードはそれに対応していない。

例えば、 2023-11-23T10:29:15.53+09:00 のように、ミリ秒まで含まれるISO8601拡張形式を処理したい場合は

let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withFractionalSeconds]

のように .withFractionalSeconds オプションを追加で指定する必要がある。他にも拡張形式は様々あるため、拡張形式でDateが表記されているレスポンスの場合、ISO8601DateTranscoderではうまく処理できないことがある。

対策

DateTranscoder はprotocolとして公開されているので、自分で DateTranscoder を作ってしまえば良い。

struct CustomISO8601DateTranscoder: DateTranscoder {
    public func encode(_ date: Date) throws -> String { ISO8601DateFormatter().string(from: date) }

    public func decode(_ dateString: String) throws -> Date {
        let iso8601DateFormatter = ISO8601DateFormatter()
        iso8601DateFormatter.formatOptions = [.withFractionalSeconds]

        guard let date = iso8601DateFormatter.date(from: dateString) else {
            throw DecodingError.dataCorrupted(
                .init(codingPath: [], debugDescription: "Expected date string to be ISO8601-formatted.")
            )
        }
        return date
    }
}

実装した CustomISO8601DateTranscoder はConfiguration経由でClientに渡せば良い。

let transport = AsyncHTTPClientTransport(configuration: .init())
let client = Client(
    serverURL: serverURL,
    configuration: Configuration(dateTranscoder: CustomISO8601DateTranscoder()),
    transport: transport
)

iso8601DateFormatter.formatOptions にどのOptionを指定すればよいかはAPIのレスポンス形式による。使えるOptionはこちらを参照すること。

developer.apple.com

参考ページ