Get value after assigning [String: Any?] to [String: Any]

TL:DR: Is there any way to unwrap value from v2 after such conversion? :

  let v1: Any? = "123"
  let v2: Any = v1

Long story:
I have a library which gives me a dictionary with type [String: Any?].
After that, I’m trying to give it to mapping library which expects dictionary with type [String: Any]
So at some point when the mapping lib tries to get values if fails because it can’t understand that some value actually an optional and not just Any value.

Every time when I try to cast the value from Any to Optional is wraps it in another Optional. So instead get this

    let d1: [String: Any?] = ["1": Optional(1)]
    let d2: [String: Any] = d1

    let v1 = d1["1"]
    let v2 = d2["1"]

    print(" val: ", v1, "type: ", type(of: v1))
    print(" val: ", v2, "type: ", type(of: v2))

    let un1 = v1!!
    let un2 = v2!! //Error - Cannot force unwrap value of non-optional type 'Any'
    let v3 = v2! as Optional<Any> //Wrapping it again
    print(" val: ", v3, "type: ", type(of: v3))
    let un3 = v3!

    // The only way to get it, which I've found
    switch v2! {
    case let v as Optional<Any>:
        print(" val: ", v, "type: ", type(of: v))
        v!
    default: break
    }

But this tricky way with switch is not working in all scenarios. Maybe there is any proper way to handle this situation?

This works:

let v1: Any? = "123"
let v2: Any = v1
let v3: Any? = Mirror(reflecting: v2).descendant("some")

dump(v1)  // Optional("123") → some: "123"
dump(v2)  // Optional("123") → some: "123"
dump(v3)  // Optional("123") → some: "123"
dump(v3!) // "123"

But it’s black magic to me. Running it may eat your cat, launch the missiles or I don’t know what. Fortunately there are people who can give you an answer while knowing what they’re doing, so you may save your cat after all.

3 Likes

Rather than coercing to [String: Any] and then trying to work out which values are optional, I would just filter out the nil values from the [String: Any?] right off the bat.

For example:

extension Dictionary {
  func compactMapValues<U>(
    _ transform: (Value) throws -> U?
  ) rethrows -> [Key: U] {
    var result = [Key: U]()
    for (key, value) in self {
      result[key] = try transform(value)
    }
    return result
  }
}

let d1: [String: Any?] = ["1": Optional(1), "2": nil]
let d2: [String: Any] = d1.compactMapValues { $0 }
print(d2) // ["1": 1]

Presumably the framework you get the [String: Any?] from is a JSON parsing framework and a nil value represents a "key": null association in a JSON object. If you need this information, then filter out the nil values just before passing the dictionary off to the mapping framework; otherwise I would remove them as soon as you get the dictionary in order to avoid confusion.

But to answer the actual question of:

Any and Optional are inherently tricky to use together, as the compiler can both promote an Any to an Any?, and erase an Any? into an Any.

There’s actually currently an inconsistency in how the compiler and runtime treat casts to optional types. If you try to directly cast an Any to an Any?, the compiler will just do optional promotion without checking with the runtime that the operand isn’t already an optional:

let v1: Any? = "123"
let v2: Any = v1 as Any

// Forced cast from 'Any' to 'Any?' always succeeds; did you mean to use 'as'?
let v3 = v2 as! Any?
dump(v3) // Optional(Optional("123")) → some: Optional("123") → some: "123"

However if you use generic functions to hide the fact that the destination type is an optional, then the runtime will check to see if the operand is actually optional and extract it (if not, only then will it perform optional promotion):

func forceCast<T, U>(_ x: T, to _: U.Type) -> U {
  return x as! U
}

func optionalPromotingCast<T>(_ x: T) -> T? {
  return forceCast(x, to: T?.self)
}

let v1: Any? = "123"
let v2: Any = v1 as Any

let v3: Any? = optionalPromotingCast(v2)
dump(v3) // Optional("123") → some: "123"

let nonOptional: Any = "123"
let v4: Any? = optionalPromotingCast(nonOptional)
dump(v4) // Optional("123") → some: "123"

IMO the runtime has the correct behaviour, and v2 as! Any? should do the same thing as the generic workaround; it should check the dynamic type of v2 first and perform optional promotion last.

You could easily argue against this though, as this is a cast that always succeeds, and casts that always succeed are done using the as operator in Swift. If as checked if the operand was optional before optional promotion, then it would be inconsistent with the implicit optional promotion behaviour when assigning an Any to an Any?. Like I said earlier, this is an inherently tricky problem.

For now though you should be able to use the generic workaround to solve your problem; though I would still advise avoiding the problem altogether by filtering out the nil values when you have an easy chance to do so.

4 Likes

Thank you for the great answer!

It’s a good point about throwing away nil values, but unfortunately, it is not that simple as just plain Dictionary. You are right it came from parsing, but parsing JSON from GraphQL server, which means that optionals are everywhere, literally. It can be a dictionary with an optional array containing optional values containing dictionaries with optional values… and so on.

About your explanation. Yes, I agree that this behavior is not pretty consistent especially if considering my discovery about switch statement. Probably switch also checks type in runtime.

The proposed solution is brilliant! It’s like discovering that Santa Claus is your parents :)
Unfortunately, it breaks when you try to give it not “covered” optional v1. It wraps it again and makes Optional<Optional>

I’ve ended up with some dusk tape solution similar to the @zoul proposal

func unwrapIfPossible(_ any:Any) -> Any {
    let mirror = Mirror(reflecting: any)
    guard mirror.displayStyle == .optional, let first = mirror.children.first else {
        return any
    }
    return first.value
}

I’ve tried more “not debug property” solution with subjectType… but it works only with primitive types, because turns out Optional is not Optional :(

internal func unwrap(_ any:Any) -> Any {
    var obj = any
    var mirror = Mirror(reflecting: obj)
    while mirror.subjectType == Optional<Any>.self {
        guard let value = mirror.children.first?.value else {
            break
        }
        obj = value
        mirror = Mirror(reflecting: obj)
    }
    return obj
}

But for example, if you first set it to Optional it starts working which is pretty correct

let a1 = Optional(["asd"])
let a2: Optional<Any> = a1

print(unwrap(a1)) // Optional(["asd"])
print(unwrap(a2)) // ["asd"]

I’ve tried something with ‘is’ and ‘as’, but still no luck. Maybe someone in comunity can solve this riddle. I’ve found my good enough solution which will probably work till Swift update, but still it is really interesting issue to solve

Sorry for taking so long to reply!

I’m not sure I understand why you’d want to do this; if you know that the argument’s static type is already the same optionality as its dynamic type, then you don’t need to pass it to optionalPromotingCast, as that will just perform optional promotion.

The behaviour of returning Any?? for an Any? argument was intentional; it allows you to dig out an Any?? from an Any?:

let foo: Any?? = "123"
let bar: Any? = foo as Any

let baz: Any?? = optionalPromotingCast(bar)
dump(baz) // Optional(Optional("123")) → some: Optional("123") → some: "123"

It’s worth noting that you can express your unwrapIfPossible(_:) function in terms of optionalPromotingCast(_:), as we’re just returning the argument in the case where the underlying optional is nil:

func unwrapIfPossible(_ x: Any) -> Any {
  return optionalPromotingCast(x) ?? x
}

let v1: Any? = "123"
let v2: Any = v1 as Any

let v3 = unwrapIfPossible(v2)
dump(v3) // "123"

Sorry, but what is optionalPromotingCast(_:)? I find no reference to it anywhere other than here, and it doesn't seem to be recognized by the compiler.

It's example code from three messages earlier.

Terms of Service

Privacy Policy

Cookie Policy