A common point of confusion in the Swift encoding and decoding ecosystem is that there is an Encoder protocol, but standard library types like JSONEncoder and PropertyListEncoder don’t conform to it. We encounter the Encoder protocol when we make our types conform to Encodable:

/// A type that can encode itself to an external representation.
public protocol Encodable {
    /// Encodes this value into the given encoder. 
    public func encode(to encoder: Encoder)
}

The protocol requires us to implement an encode(to:) method, which takes in an Encoder.

What’s going on here? 🤔🤔🤔

The answer lies in the architecture of JSONEncoder.

Inspecting the source code for JSONEncoder, we see it’s a open type that internally uses a private type _JSONEncoder, which does conform to Encoder.


JSONEncoder uses a private _JSONEncoder.



The actual code is rather complicated, but it boils down to something like this:

class JSONEncoder {
    func encode<T: Encodable>(_ value: T) throws -> Data {
        let privateEncoder = _JSONEncoder()
        try value.encode(to: privateEncoder)
        // do some processing to convert `privateEncoder`'s contents
        // to `Data`, and return it
    }
}

So that encode(to:) method you wrote for your Encodable type is being used – it’s just that a _JSONEncoder is passed in, and not a JSONEncoder.

The same is true on the decoding side - there is a Decoder protocol, which we see in the Decodable protocol:

/// A type that can decode itself from an external representation.
public protocol Decodable {
  /// Creates a new instance by decoding from the given decoder.
  init(from decoder: Decoder) throws
}

But JSONDecoder and PropertyListDecoder don’t conform to Decoder, and instead internally use private Decoder types.


JSONDecoder uses a private _JSONDecoder.



In code, it plays out similarly to the encoding case:

class JSONDecoder {
    func decode<T: Decodable>(_ type: T.Type, from data: Data) throws -> T {
        let privateDecoder = _JSONDecoder()
        // do some processing to store `data` in `privateDecoder`'s storage
        return try T(from: privateDecoder)
    }
}

Ok… but why?

Ok, so now we see how the Encodable and Decodable protocols fit into JSONEncoder and JSONDecoder. But why were they designed that way? Why not just make JSONEncoder an Encoder too?

In short, the answer is that they provide very different APIs. The JSONEncoder API is designed to provide a single, simple entry point into encoding, and the Encoder protocol provides a completely different API for customizing how types are encoded.

Consider JSONEncoder. All we really get is this one simple method:

func encode<T: Encodable>(_ value: T) throws -> Data

And we know exactly what to do with it:

let s = MyType()
let sData = try JSONEncoder().encode(s)

There are also some configuration options available on JSONEncoder, such as outputFormatting and keyEncodingStrategy. But that’s about it.

On the other hand, the Encoder protocol requires a number of methods that provide containers for encoding values in:

func container<Key: CodingKey>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key>
func singleValueContainer() -> SingleValueEncodingContainer
func unkeyedContainer() -> UnkeyedEncodingContainer 

And those container types are themselves protocols, which provide their own various methods for storing values in them, like this:

protocol SingleValueEncodingContainer {
    mutating func encode(_ value: UInt16) throws
    mutating func encode(_ value: Int64) throws
    // ... repeat for several primitive Swift types
}

The methods on Encoder and SingleValueEncodingContainer (and the other container types, too) are meant to be used by Encodable types in their encode(to:) implementations to customize how exactly they’re encoded. I’ll talk about that more in a future blog post, but for now you can read about it in the Swift documentation (see “Encoding and Decoding Manually”).

One of the authors of Codable, Itai Ferber, confirmed the reasoning behind the design: Card

Conclusion

In short, JSONEncoder and JSONDecoder use internal types conforming to Encoder and Decoder in order to split up portions of the encoding/decoding APIs, based on how and where you use them.

I hope you found this post helpful! Please feel free to ask questions or give feedback via Twitter or email.