[swift-evolution] Question regarding SE-0167 Swift Encoders

Gwendal Roué gwendal.roue at gmail.com
Wed May 31 00:01:41 CDT 2017


Hello Itai,

Thanks for helping sorting things out.

I have since my initial question a better understanding of Codable, and I hope I can better express the trouble. I bump against the fact that SE-0166 and SE-0167 assume that there is a single kind of coding keys.

This is the case for JSON and Plist:
	
	{
		"a": "foo"
		"b": { ... }
	}

But imagine a different serialization format where we don't use the same kind of keys for values and objects. Values are stored on red keys, and objects on blue keys:
	
	{
		"a" (red): "foo"
		"b" (blue): { ... }
	}

This serialization format accepts keys with the same name, as long as they don't have the same color:
	
	{
		"a" (red): "foo"
		"a" (blue): { ... }
	}

This format is used by SQL rows in GRDB. A SQL row is both a set of columns with associated values (the "red" keys), plus a set of scopes with associated "view" on the row (the blue keys):

    let row = ...
    row["id"]                              // 1
    let scopedRpw = row.scoped(on: "foo")! // <Row "id":2, "foo": "bar">
    scopedRow["id"]                        // 2

If you wonder: "but why???": columns and scopes are what can make rows a suitable base for hierarchical decoding, just like JSON and PList. When a flat SQL row fetched from a joined query is seen as a hierarchical structure, several simple `init(row:)` initializers can get the rows they expect, and we load a complex graph of objects. I have high hopes (https://github.com/groue/GRDB.swift/issues/176#issuecomment-285938568).

I care about Codable because of the code generation is has been blessed with. I expect GRDB users to rush on Codable since they won't have any longer to write the decoding boilerplate.


> I have to confess that I’m not familiar with this concept, but let’s take a look:
> if let valueType = T.self as? DatabaseValueConvertible.Type {
>     // if column is missing, trigger the "missing key" error or return nil.
> } else if let complexType = T.self as? RowConvertible.Type {
>     // if row scope is missing, trigger the "missing key" error or return nil.
> } else {
>     // don't know what to do
>     fatalError("unsupported")
> }
> Is it appropriate for a type which is neither DatabaseValueConvertible nor RowConvertible to be decoded with your decoder? If not, then this warrants a preconditionFailure or an error of some sort, right? In this case, that would be valid.
> 
Yes it is, there's no point preventing this.

We can forget the GRDB DatabaseValueConvertible and RowConvertible protocals in this discussion - they have their purpose, but are irrelevant here, and I was wrong letting them in the discussion. Will you look at some updated code?

In practice, let's consider the `KeyedDecodingContainerProtocol.decode(_:forKey:)` method. The decoding container is asked for a type T. It does not know yet if T is a single-value or a keyed type. No problem: it delays the decision until the Decoder is asked for a container:
    
    struct RowKeyedDecodingContainer<Key: CodingKey>: KeyedDecodingContainerProtocol {
        func decode<T>(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable {
            // Push the key, and wait until the decoder is asked for a container
            // so that we know if T is keyed, or single-value:
            return try T(from: RowDecoder(row: row, codingPath: codingPath + [key]))
        }
    }

    struct RowDecoder: Decoder {
        func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> {
            if let key = codingPath.last {
                // Asked for a keyed type: look for a row scope
                if let scopedRow = row.scoped(on: key!.stringValue) {
                    let container = RowKeyedDecodingContainer<Key>(row: scopedRow, codingPath: codingPath)
                    return KeyedDecodingContainer(container)
                } else {
                    throw DecodingError.keyNotFound...
                }
            } else {
                // Asked for a keyed type at the top level
                let container = RowKeyedDecodingContainer<Key>(row: row, codingPath: codingPath)
                return KeyedDecodingContainer(container)
            }
        }
        
        func singleValueContainer() throws -> SingleValueDecodingContainer {
            // Asked for a single-value type: look for a column
            return RowColumnDecodingContainer(row: row, column: codingPath.last!!.stringValue)
        }
    }

(Sorry for the bangs, I still have to understand how I should deal with nil coding keys)

This works pretty well so far.

But now let's consider the `KeyedDecodingContainerProtocol.decodeIfPresent(_:forKey:)` method. Now we have a problem. This method must return nil if the key is missing. But which key? We don't know if the decoded type is keyed, or single-value. We can't postpone the decision, as above. So we have to double guess:

My current implementation is the following:

    func decodeIfPresent<T>(_ type: T.Type, forKey key: Key) throws -> T? where T : Decodable {
        if let dbValue: DatabaseValue = row[key.stringValue] {
            // We don't know if T(from: Decoder) will request a single value
            // container, or a keyed container.
            //
            // Since the column is present, let's assume that T will ask for a
            // single value container (a column). This is our only opportunity
            // to turn NULL into nil. If T eventually asks for a keyed container
            // (a row scope), then the user will face a weird error.
            if dbValue.isNull {
                return nil
            } else {
                return try T(from: RowDecoder(row: row, codingPath: codingPath + [key]))
            }
        } else if row.scoped(on: key.stringValue) != nil {
            // We don't know if T(from: Decoder) will request a single value
            // container, or a keyed container.
            //
            // Since the row scope is present, let's assume that T will ask for
            // a keyed container (a row scope). If T eventually asks for a
            // single value container (a column), then the user will face a
            // weird error.
            return try T(from: RowDecoder(row: row, codingPath: codingPath + [key]))
        } else {
            // Both column and row scope are missing: we are sure that the value
            // is missing.
            return nil
        }
    }

But it's less than ideal, as expressed by the inline comments.

We could discuss potential solutions, but I first hope that I was able to clearly express the topic.

Gwendal

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170531/fe681a10/attachment.html>


More information about the swift-evolution mailing list