Decode a JSON object of unknown format into a Dictionary with Decodable in Swift 4

Getting back the underlying data from JSONSerialization is the exact idea behind Unevaluated — the difference between AnyCodable and Unevaluated is that AnyCodable attempts to decode types that it knows about on its own and is performing conversions; on the other hand, Unevaluated would be a marker type which asks the Decoder to stick whatever existing representation it has of the underlying data into its .value and returns that. If the Decoder supports Unevaluated (e.g. in formats where it's possible to grab an underlying representation like NSNulls, Strings, etc.), it can do that; if the Decoder does nothing special to recognize Unevaluated and ends up calling its init(from:), Unevaluated will just throw a .typeMismatch letting you know that the Decoder doesn't support it.

To make this concrete, the following implementation of unbox is how JSONDecoder handles taking an existing container and coercing it into the value you've asked for:

fileprivate func unbox<T : Decodable>(_ value: Any, as type: T.Type) throws -> T? {
    if type == Date.self || type == NSDate.self {
        return try self.unbox(value, as: Date.self) as? T
    } else if type == Data.self || type == NSData.self {
        return try self.unbox(value, as: Data.self) as? T
    } else if type == URL.self || type == NSURL.self {
        guard let urlString = try self.unbox(value, as: String.self) else {
            return nil
        }

        guard let url = URL(string: urlString) else {
            throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath,
                                                                    debugDescription: "Invalid URL string."))
        }

        return url as! T
    } else if type == Decimal.self || type == NSDecimalNumber.self {
        return try self.unbox(value, as: Decimal.self) as? T
    } else {
        self.storage.push(container: value)
        defer { self.storage.popContainer() }
        return try type.init(from: self)
    }
}

The value passed in to the method is the value returned from JSONSerialization (e.g. NSDictionary containing NSString and NSArray); today, JSONDecoder knows about a few special types and intercepts them to reinterpret the data. If we were unbox(value, as: Unevaluated.self) today, we'd fall into that last case:

self.storage.push(container: value)
defer { self.storage.popContainer() }
return try type.init(from: self)

That last line would end up calling Unevaluated.init(from:), which would just throw a .typeMismatch. In order to support Unevaluated, we'd expand unbox to do this:

fileprivate func unbox<T : Decodable>(_ value: Any, as type: T.Type) throws -> T? {
    if type == Unevaluated.self {
        return Unevaluated(value)
    } else if ... {
        // ...
    } else {
        self.storage.push(container: value)
        defer { self.storage.popContainer() }
        return try type.init(from: self)
    }
}

This would just return an Unevaluated instances whose contents are exactly what JSONSerialization returned: the collection of NS values it decoded. When you get back the Unevaluated type, its .value contains exactly what you're getting at. (This is the raw value access you're looking for.)