How to make sure that all JSON fields are presented in a Decodable model?

Let’s imagine a third-party JSON API returning a model like this:

{
  "key1": "Hello"
}

So we describe it as follows:

struct Model: Codable {
  var key1: String
}

Eventually the JSON model gets a new field…

{
  "key1": "Hello",
  "key2": {
    "key3": "World"
  }
}

…and the old Model still works, but it misses a new property key2.

Is there is any way to verify that all JSON keys are presented in the Decodable model?

Update: Solved, thanks!

2 Likes

There's perhaps a few ways you can do it. Off my head, you can use a wrapper struct perhaps, say named AnyModel that conforms to Decodable and works with any model that conforms to Decodable.
In the example below, generic Decodable type T would be Model from your example.

struct AnyModel<T: Decodable>: Decodable {
    let result: Result<T, Error>
    init(from decoder: Decoder) throws {
        do {
            let decoded = try T.init(from: decoder)
            result = .success(decoded)
         } catch let error {
            result = .failure(error)
        }
    }
 }

But that won't tell you if there are missing fields I think, but won't fail too. My favorite is using Protocol buffers as I used protocol buffers for everything. Probably an overkill for your use case.

import SwiftProtobuf

extension Message {
    public init?(_ data: Data) {
        var jsonOptions = JSONDecodingOptions()
        jsonOptions.ignoreUnknownFields = false
        do {
            let message = try Self(jsonUTF8Data: data, options: jsonOptions)
            self = message
        } catch let err {
            guard let error = err as? JSONEncodingError else { return nil }
            switch error {
            case .missingValue:
                print("missing JSON field error: ", error)
            default:
                print("some other JSONEncodingError: ", error)
            }
            return nil
        }
    }
}

There could be other options too!

1 Like

Thanks for the reply, I would prefer to avoid vendor code. It turns out, something like this does the trick:

let dict = try JSONSerialization.jsonObject(with: data) as? [String: Any]
let result = try JSONDecoder().decode(Model.self, from: data)
if let keys = dict?.keys {
  let mirror = Mirror(reflecting: result)
  if keys.count != mirror.children.count {
    print("Wrong number of keys") 
  } 
}

Update: Even better, the JSONSerialization can be replaced by keyDecodingStrategy.custom to collect all the keys.

Ah, yes, Mirror works great! Nice approach!

You can also wrap model with this

protocol StrictDecodable: Decodable {
    associatedtype CodingKeys: CodingKey
}

struct Strict<T>: Decodable where T: StrictDecodable {
    var value: T

    init(from decoder: Decoder) throws {
        value = try T(from: decoder)

        let allKeys = try decoder.container(keyedBy: Keys.self).allKeys
        let usableKeys = try decoder.container(keyedBy: T.CodingKeys.self).allKeys
        if totalKeys.count != usableKeys.count {
            throw DecodingError.typeMismatch(T.self, .init(codingPath: decoder.codingPath, debugDescription: "Strict \(T.self) does not use all keys from decoder"))
        }
    }

    struct Keys: CodingKey {
        init?(stringValue: String) { }
        init?(intValue: Int) { }
        var stringValue: String { return "" }
        var intValue: Int? { return nil }
    }
}

Like this

struct Model: Codable, StrictDecodable {
    var key1: String

    enum CodingKeys: CodingKey {
        case key1
    }
}

try decoder.decode(Model.self, from: data.data(using: .utf8)!) // Model(key1: "Hello")
try decoder.decode(Strict<Model>.self, from: data) // throw DecodingError

One caveat is that you must explicitly declare CodingKeys.


Or if you can use Mirror

struct Strict<T>: Decodable where T: Decodable {
    var value: T

    init(from decoder: Decoder) throws {
        value = try T(from: decoder)

        let allKeys = try decoder.container(keyedBy: AllKeys.self).allKeys
        let usableKeys = Mirror(reflecting: value).children
        if allKeys.count != usableKeys.count {
            throw DecodingError.typeMismatch(T.self, .init(codingPath: decoder.codingPath, debugDescription: "Strict \(T.self) does not use all keys from decoder"))
        }
    }

    struct AllKeys: CodingKey {
        init?(stringValue: String) { }
        init?(intValue: Int) { }
        var stringValue: String { return "" }
        var intValue: Int? { return nil }
    }
}

And simply do

struct Model: Codable {
    var key1: String
}

let value = try decoder.decode(Strict<Model>.self, from: data)

Make sure that Mirror reflects the appropriate amount of keys you have. It is possible to have different number of variable if the CodingKey is not synthesised.

Thing could look much better once we get Property Wrapper.

1 Like