swiftgrpc-swiftswiftprotobuf

Swift Protobuf Codable Extension


We're using Swift Protobuf in a project that generates the message struct code (i.e. DTOs). It doesn't implement Codable and I'm trying to add Codable through an extension so that I can serialize them to disk when used inside a non-protobuf that conforms to Codable. However, it's quite boilerplate to do this for every struct.

Any recommendations on how to minimize the boiler plate code, other than using a static, generic method? Is there a way to do this using generics in the extension definition? Ideally, I'd like to replace Message1 and Message2 with a generic extension.

With Swift Protobuf all message structs have a Message extension applied, but they do not have a common protocol I can use in a where clause for an extension.

extension Message1: Codable {
    public init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let data = try container.decode(Data.self)
        self = try Message1(serializedData: data)
    }
    
    public func encode(to encoder: Encoder) throws {
        let data = try self.serializedData()
        var container = encoder.singleValueContainer()
        try container.encode(data)
    }
}

extension Message2: Codable {
    public init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let data = try container.decode(Data.self)
        self = try Message2(serializedData: data)
    }

    public func encode(to encoder: Encoder) throws {
        let data = try self.serializedData()
        var container = encoder.singleValueContainer()
        try container.encode(data)
    }    
}

Solution

  • First, declare a protocol CodableMessage that refines both SwiftProtobuf.Message and Codable. Use an extension to provide default implementations of init(from:) and encode(to:).

    // Use this narrow import to avoid importing SwiftProtobuf.Decoder, since
    // that will conflict with Swift.Decoder and generally be annoying.
    import protocol SwiftProtobuf.Message
    
    public protocol CodableMessage: SwiftProtobuf.Message, Codable { }
    
    extension CodableMessage {
        public init(from decoder: Decoder) throws {
            let container = try decoder.singleValueContainer()
            let data = try container.decode(Data.self)
            self = try Self(serializedData: data)
        }
    
        public func encode(to encoder: Encoder) throws {
            let data = try self.serializedData()
            var container = encoder.singleValueContainer()
            try container.encode(data)
        }
    }
    

    Then, extend each of your generated message types to conform to your new protocol:

    extension Message1: CodableMessage { }
    extension Message2: CodableMessage { }