[SOLVED] Forcing an Encoder's UnkeyedEncodingContainer to only contain one type of value

Issue

As part of a custom Encoder, I am coding an UnkeyedEncodingContainer. However, the specific format I am making it for asks that all elements of an array be of the same type. Specifically, arrays can contain :

  • Integers one same size
  • Floats or Doubles
  • Other arrays (not necessarily all containing the same kinds of elements)
  • Objects
    Here is the type of answer I need : The basis of an UnkeyedEncodingContainer implementation that conforms to the protocol, and enforces that all elements be of one same type among the above specified ones.

As requested, here are examples of things that should or should not be encodable :

var valid1 = []
var valid2 = [3, 3, 5, 9]
var valid3 = ["string", "array"]

var invalid1 = [3, "test"]
var invalid2 = [5, []]
var invalid3 = [[3, 5], {"hello" : 3}]
// These may not all even be valid Swift arrays, they are only
// intended as examples

As an example, here is the best I have come up with, which does not work :


The UnkeyedEncodingContainer contains a function, checkCanEncode, and an instance variable, ElementType :

var elementType : ElementType {
    if self.count == 0 {
        return .None
    } else {
        return self.storage[0].containedType
    }
}


func checkCanEncode(_ value : Any?, compatibleElementTypes : [ElementType]) throws {
    guard compatibleElementTypes.contains(self.elementType) || self.elementType == .None else {
        let context = EncodingError.Context(
            codingPath: self.nestedCodingPath,
            debugDescription: "Cannot encode value to an array of \(self.elementType)s"
        )
        throw EncodingError.invalidValue(value as Any, context)
    }
}
// I know the .None is weird and could be replaced by an optional,
// but it is useful as its rawValue is 0. The Encoder has to encode
// the rawValue of the ElementType at some point, so using an optional
// would actually be more complicated

Everything is then encoded as a contained singleValueContainer :

func encode<T>(_ value: T) throws where T : Encodable {
    let container = self.nestedSingleValueContainer()
    try container.encode(value)
    try checkCanEncode(value, compatibleElementTypes: [container.containedType])
}
// containedType is an instance variable of SingleValueContainer that is set
// when a value is encoded into it

But this causes an issue when it comes to nestedContainer and nestedUnkeyedContainer : (used for stored dictionaries and arrays respectively)

// This violates the protocol, this function should not be able to throw
func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type) throws -> KeyedEncodingContainer<NestedKey> where NestedKey : CodingKey {
    let container = KeyedContainer<NestedKey>(
        codingPath: self.nestedCodingPath,
        userInfo: self.userInfo
    )
    try checkCanEncode(container, compatibleElementTypes: [.Dictionary])
    self.storage.append(container)
    return KeyedEncodingContainer(container)
}

As you can see, since I need checkCanEncode to know whether it is even possible to create a NestedContainer in the first place (because if the array already has stuff inside that aren’t dictionaries, then adding dictionaries to it is invalid), I have to make the function throw. But this breaks the UnkeyedEncodingContainer protocol which demands non-throwing versions.

But I can’t just handle the error inside the function ! If something tries to put an array inside an array of integers, it must fail. Therefore this is an invalid solution.


Additional remarks :

Checking after having encoded the values already feels sketchy, but checking only when producing the final encoded payload is definitely a violation of the "No Zombies" principle (fail as soon as the program enters an invalid state) which I would rather avoid. However if no better solution is possible I may accept it as a last resort.

One other solution I have thought about is encoding the array as a dictionary with numbered keys, since dictionaries in this format may contain mixed types. However this is likely to pose decoding issues, so once again, it is a last resort.


You will be advised not to edit other people’s questions. If you have edits to suggest please do so in the comments, otherwise mind your own business

Solution

Unless anyone has a better idea, here is the best I could come up with :

  • Do not enforce that all elements be of the same type inside the UnkeyedEncodingContainer
  • If all elements are the same type, encode it as an array
  • If elements have varying types, encode it as a dictionary with integers as keys

This is completely fine as far as the encoding format goes, has minimal costs and only slightly complicates decoding (check whether keys contain integers) and greatly widens how many different Swift object will be compatible with the format.

Note : Remember that the "real" encoding step where the data is generated is not actually part of the protocol. That is where I am proposing the shenanigans should take place 😈

Answered By – Crysambrosia

Answer Checked By – Clifford M. (BugsFixing Volunteer)

Leave a Reply

Your email address will not be published.