-1

I have a Swift model using Codable, and my backend is inconsistent with date formats:

Sometimes a date comes as ISO8601 string ("2025-09-30T04:00:00Z")

Sometimes as a Unix timestamp (1696036800)

Sometimes as null.

Right now, I’m repeating custom decoding logic in each model:

struct Event: Codable {
    let createdAt: Date?

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let string = try? container.decode(String.self),
           let date = ISO8601DateFormatter().date(from: string) {
            createdAt = date
        } else if let timestamp = try? container.decode(Double.self) {
            createdAt = Date(timeIntervalSince1970: timestamp)
        } else {
            createdAt = nil
        }
    }
}

This works, but it duplicates code across models.

Question: How can I move this decoding logic into a @propertyWrapper, so I can reuse it like this?

struct Event: Codable {
    @FlexibleDate var createdAt: Date?
}
5
  • The real solution is to fix the backend. The standard date representation in JSON is ISO8601. All JSON serializers will handle ISO8601. None will treat raw integers as second-based Unix timestamps (who says the number isn't milliseconds?). Around 2000 Microsoft used the /Date(1224043200000)/ format to pass Unix timestamps as unambiguous text, at a time when even JavaScript didn't support ISO8601 properly, but by 2012 most people used ISO8601, and by 2015 this became the actual standard. 1696036800 is an unusual value Commented Oct 1 at 13:54
  • If you which formats you need to handle you may check the string against regular expressions before trying to make the conversion. Commented Oct 1 at 14:02
  • 1
    stackoverflow.com/questions/44682626/… ? Commented Oct 1 at 14:11
  • "I want a cleaner solution..." is not a question Commented Oct 1 at 14:20
  • @JoakimDanielson, thanks for pointing this out. I’ll edit the question to clarify the specific problem I was facing. Commented Oct 2 at 6:01

2 Answers 2

1

Your current code is already very close to a property wrapper. All you need to do is rename Event to FlexibleDate, and createdAt to wrappedValue, and finally add the @propertyWrapper attribute.

@propertyWrapper
struct FlexibleDate: Codable {
    let wrappedValue: Date?

    init(from decoder: any Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let string = try? container.decode(String.self),
           let date = try? Date.ISO8601FormatStyle().parse(string) {
            wrappedValue = date
        } else if let timestamp = try? container.decode(Double.self) {
            wrappedValue = Date(timeIntervalSince1970: timestamp)
        } else {
            wrappedValue = nil
        }
    }
}

Note that I have also changed to use Date.ISO8601FormatStyle instead of ISO8601DateFormatter, so that you don't unnecessarily allocate memory for the date formatter every time this is decoded.

Sign up to request clarification or add additional context in comments.

Comments

0

You should never throw away the errors specially when decoding JSON. Just catch the error and try again. It will also allow you to declare your property non optional:

@propertyWrapper
struct FlexibleDate: Codable {
    let wrappedValue: Date
    public init(from decoder: any Decoder) throws {
        let container = try decoder.singleValueContainer()
        do {
            wrappedValue = Date(timeIntervalSince1970: try container.decode(Double.self))
        } catch let DecodingError.typeMismatch(type, _) where type == Double.self {
            wrappedValue = try Date(container.decode(String.self), strategy: .iso8601)
        }
    }
    public func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(wrappedValue)
    }
}

If you need an optional wrapper as well you can do as follow:

@propertyWrapper
struct OptionalFlexibleDate: Codable {
    let wrappedValue: Date?
    public init(from decoder: any Decoder) throws {
        self.wrappedValue = try decoder.singleValueContainer().decodeNil() ? 
            nil : FlexibleDate(from: decoder).wrappedValue
    }
    public func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(wrappedValue)
    }
}

Usage:

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        do {
//            let encoder = JSONEncoder()
//            encoder.dateEncodingStrategy = .iso8601  // or .secondsSince1970
//            let jsonData = try encoder.encode(AFlexibleDate(createdAt: .now, modifiedAt: .now))
            let jsonString = #" {"createdAt":"2025-10-02T17:25:23Z","modifiedAt":1759425923.0}"#
            let jsonData = Data(jsonString.utf8)
            let event = try JSONDecoder().decode(AFlexibleDate.self, from: jsonData)
            print(event.createdAt)  // 2025-10-02 17:25:23 +0000
            print(event.modifiedAt ?? "nil") // 2025-10-02 17:25:23 +0000
        } catch {
            print(error)
        }
    }
}

struct AFlexibleDate: Codable {
    @FlexibleDate var createdAt: Date
    @OptionalFlexibleDate var modifiedAt: Date?
}

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.