Are custom Encoders/Decoders supported?

Are we supposed to be able to write our own classes like JSONDecoder? I'm writing code for CloudKit and it looks an awful lot like Codable code, so I wondering if I can do this:

// let decoder = JSONDecoder()
// let foo = decoder.decode(Foo.self, from: someJsonData)
let decoder = MyCloudKitRecordDecoder()
let foo = decoder.decode(Foo.self, from: ckRecord)

The benefit would be that it would reuse my type's init(from:Decoder) and encode(to:Decoder) methods and I wouldn't have to write similar things for CloudKit/CKRecord. I have another similar situation with local SQLite queries.

I looked at the docs and the only thing JSONDecoder implements is TopLevelDecoder, which has one little method. I don't see how that could be enough to get this to work.

1 Like

I wrote some coders. There's also CodableCSV. I don't know if anyone did CouldKit specifically, but at least you could take a look at these.

1 Like

You do not strictly need to follow the model of JSONDecoder, which mostly provides just a top-level interface. The key thing for interacting with Codable types is to implement the Encoder and Decoder protocols with your own types that handle the low-level serialization and deserialization. You can write any kind of type you want as an interface on top.

Ah, now I see. I think I got confused because JSONDecoder is not a Decoder.

I'm curious why that is. Is there a benefit to the current design vs. something like this:

let decoder = JSONDecoder(from: data)
let foo = Foo(from: decoder)

I'm not sure why Foundation's JSONDecoder interface is designed the way it is. Maybe @Philippe_Hausler has the details.

They are not decoders, they are something else - they are what is referred to as a TopLevelDecoder, in that they are the thing that encapsulates the creation of decoders. It boils down to the transform concept from the input type (in this case Data) to a structured form (a type). The design reasons were sourced from needing a common form without needing associated types.

For reference Combine currently defines the concept of TopLevelDecoder, it was something we realized needs to be perhaps sunk a bit lower. It seems like something that belongs in the standard library.

To put what @Philippe_Hausler says in another way — JSONDecoder is not a Decoder because the Decoder interface you want to use inside of init(from:) is different from the interface that's useful at code level you're creating a JSONDecoder in. At that top level, you don't necessarily want to deal with containers: instead of creating a JSONDecoder from a data blob, creating a SingleValueDecodingContainer from it, and decoding a value, the preferred level of abstraction and simplicity is decode(_:from:).

From an old discussion:

However, since that discussion, increased benefit for a top-level encoder/decoder concept led to the introduction of TopLevelEncoder/TopLevelDecoder.

3 Likes

@Philippe_Hausler @itaiferber Thanks for the background! Given your description of the difference in interface between a top-level coder and the Decoder protocol, could that be factored into a generic TopLevelDecoder<T: Decoder> type that implements the top-level container instantiation boilerplate, instead of having a separate type for every format with a new protocol trying to abstract over them?

Im not sure a generic on the decoder would be useful here, instead I think there should be a generic on the input since that is the smallest similar characteristic on decoders, likewise for the output for encoders.

Combine defines them as this:

@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public protocol TopLevelDecoder {
    associatedtype Input
    func decode<T: Decodable>(_ type: T.Type, from: Input) throws -> T
}

@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public protocol TopLevelEncoder {
    associatedtype Output
    func encode<T: Encodable>(_ value: T) throws -> Output
}

Now I am not sure the implications of moving a protocol like that down, but I think if we can keep it the smallest delta from what exists it can serve both the purpose of servicing combine but also servicing other use cases too.

1 Like

Along with what @Philippe_Hausler says, the primary technical difficulty I can see to doing this is that the types actually conforming to Encoder/Decoder are often private, e.g. __JSONEncoder, __JSONDecoder, __PlistEncoder, __PlistDecoder. These are instantiated within the top-level encode(_:)/decode(_:from:) implementation in each top-level coder instead of being exposed directly to API consumers.