Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Override Decodable for a Particular Type

Is there a way to override the Decodable implementation for a particular type, such that it gets used that type is part of a Codable struct?

Specifically, I have this struct:

struct Activity: Codable {
    let id: Int
    let name: String
<...>
    let distance: Measurement<UnitLength>
}

I'd like a way to provide an decoding initializer like this:


extension Measurement where UnitType == UnitLength {
    public init(from decoder: Decoder) throws {
        self = Measurement(value: try decoder.singleValueContainer().decode(Double.self), unit: UnitLength.meters)
    }
}

So that I can convert a Double value into a Measurement<UnitLength> on decoding, without having to provide a custom init(from decoder: Decoder) for each struct that has a Measurement in it.

That init compiles fine, but doesn't seem to be called from the standard Codable decoding process.

My workaround implementation is this init for the Activity struct:

init(from decoder: Decoder) throws {
    let values = try decoder.container(keyedBy: CodingKeys.self)

<...>
    distance = Measurement(value: try values.decode(Double.self, forKey: .distance), unit: UnitLength.meters)
}

But I'd much rather implement it once on Measurement than for each type that might use it.

like image 977
robmathers Avatar asked Sep 01 '25 01:09

robmathers


1 Answers

One solution that may meet your needs to is to define your own measurement struct that assumes a length measurement in meters.

struct MyMeasurement {
    let length: Measurement<UnitLength>
}

extension MyMeasurement: Codable {
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        length = Measurement(value: try container.decode(Double.self), unit: UnitLength.meters)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(length.value)
    }
}

Then update your Activity struct to use MyMeasurement:

struct Activity: Codable {
    let id: Int
    let name: String
<...>
    let distance: MyMeasurement
}

Then you can decode normally:

let decoder = JSONDecoder()
do {
    let activity = try decoder.decode(Activity.self, from: someJSONData)
    print(activity.distance.length)
} catch {
    print(error)
}

You probably want a better name for MyMeasurement that hints to it being a length in meters.

You can reuse MyMeasurement in any struct that has some length property in meters without the need to write a custom init(from:) for that struct.

like image 178
rmaddy Avatar answered Sep 04 '25 01:09

rmaddy