Need some help with more complicated json, with the newest swift4.1 encoder/decoder:
struct:
struct LMSRequest: Decodable {
let id : Int?
let method : String?
let params : [String]?
enum CodingKeys: String, CodingKey {
case id = "id"
case method = "method"
case params = "params"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decodeIfPresent(Int.self, forKey: .id)
method = try values.decodeIfPresent(String.self, forKey: .method)
params = try values.decodeIfPresent([String].self, forKey: .params)
}}
json:
let json = """
{
"id": 1,
"method": "slim.request",
"params": [
"b8:27:eb:db:6d:62",
[
"serverstatus",
"-",
1,
"tags:GPASIediqtymkovrfijnCYXRTIuwxNlasc"
]
]
}
""".data(using: .utf8)!
code:
let decoder = JSONDecoder()
let lms = try decoder.decode(LMSRequest.self, from: json)
print(lms)
Error is expected to decode string but found array instead. It's coming from the nested array within the "params" array... really stuck on how to build this out, Thanks!
Given what you've described, you should store params as an enum like this:
enum Param: CustomStringConvertible {
case string(String)
case int(Int)
case array([Param])
var description: String {
switch self {
case let .string(string): return string
case let .int(int): return "\(int)"
case let .array(array): return "\(array)"
}
}
}
A param can either be a string, an int, or an array of more params.
Next, you can make Param Decodable by trying each option in turn:
extension Param: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let string = try? container.decode(String.self) {
self = .string(string)
} else if let int = try? container.decode(Int.self) {
self = .int(int)
} else {
self = .array(try container.decode([Param].self))
}
}
}
Given this, there's no need for custom decoding logic in LMSRequest:
struct LMSRequest: Decodable {
let id : Int?
let method : String?
let params : [Param]?
}
As a side note, I would carefully consider whether these fields are all truly optional. It's very surprising that id is optional, and quite surprising that method is optional, and slightly surprising that params are optional. If they're not really optional, don't make them optional in the type.
From your comments, you're probably misunderstanding how to access enums. params[1] is not a [Param]. It's an .array([Param]). So you have to pattern match it since it might have been a string or an int.
if case let .array(values) = lms.params[1] { print(values[0]) }
That said, if you're doing this a lot, you can make this simpler with extensions on Param:
extension Param {
var stringValue: String? { if case let .string(value) = self { return value } else { return nil } }
var intValue: Int? { if case let .int(value) = self { return value } else { return nil } }
var arrayValue: [Param]? { if case let .array(value) = self { return value } else { return nil } }
subscript(_ index: Int) -> Param? {
return arrayValue?[index]
}
}
With that, you can say things like:
let serverstatus: String? = lms.params[1][0]?.stringValue
Which is probably closer to what you had in mind. (The : String? is just to be clear about the returned type; it's not required.)
For a more complex and worked-out example of this approach, see my generic JSON Decodable that this is a subset of.
Related
I have json response where only just one key name change rest is same and want to parse without duplicating same struct again.
"attributes": {
"symbol":"EUR",
"name":"Euro",
"precision":2,
}
"attributes":{
"symbol":"EUR",
"name":"Euro",
"precision_for_fiat_price":2,
}
How can handle this precision key dynamically in json parsing
You can use a custom keyDecodingStrategy.
Essentially, you write some logic that checks whether the current coding key path matches some criteria, and if it does, map that key to the precision key.
For example:
struct Root : Codable {
let attributes: Attributes
}
struct Attributes : Codable {
let symbol: String
let name: String
let precision: Int
enum CodingKeys: CodingKey {
case symbol
case name
case precision
}
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .custom({
keys in
// This will decode every precision_for_fiat_price key in the json as "precision".
// You might not want this.
// Make this criteria stricter if you need to. An example is shown below
if keys.last?.stringValue == "precision_for_fiat_price" {
return Attributes.CodingKeys.precision
}
// this will only decode those precision_for_fiat_price that have "attribute" as their parent as "precision"
// if stringPath.suffix(2) == ["attributes", "precision_for_fiat_price"] {
// return Attributes.CodingKeys.precision
// }
return keys.last!
})
let json = """
{
"attributes":{
"symbol":"EUR",
"name":"Euro",
"precision_for_fiat_price":2
}
}
""".data(using: .utf8)!
let decoded = try decoder.decode(Root.self, from: json)
If you want to just decode json model like this:
let json = """
{
"attributes": {
"symbol":"EUR",
"name":"Euro",
"precision_for_fiat_price":2 // or "precision": 2
}
}
"""
You can create Decodable struct:
struct WrapperModel: Decodable { // any model
var attributes: Attributes
}
struct Attributes : Decodable {
let symbol: String
let name: String
var precision: Int = 0
enum CodingKeys: String, CodingKey, CaseIterable {
case symbol
case name
case precision
case precisionAnother = "precision_for_fiat_price"
// you can write any types of key here
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
symbol = try container.decode(String.self, forKey: .symbol)
name = try container.decode(String.self, forKey: .name)
if let precisionValue = try container.decodeIfPresent(Int.self, forKey: .precision) {
precision = precisionValue
}
if let precisionValue = try container.decodeIfPresent(Int.self, forKey: .precisionAnother) {
precision = precisionValue
}
}
}
You can test it with:
let jsonData = Data(json.utf8)
let decoder = JSONDecoder()
do {
let attributes = try decoder.decode(WrapperModel.self, from: jsonData)
print(attributes)
} catch {
print(error.localizedDescription)
}
I have a Swift struct that looks like this:
struct MyStruct: Codable {
var id: String
var name: String
var createdDate: Date
}
To that, I would like to add another property: a [String:Any] dictionary. The result would look like this:
struct MyStruct: Codable {
var id: String
var name: String
var createdDate: Date
var attributes: [String:Any] = [:]
}
In the end, I would like to be able to serialize my MyStruct instance to a JSON string, and vice versa. However, when I go to build I get an error saying,
Type 'MyStruct' does not conform to protocol 'Codable'
Type 'MyStruct' does not conform to protocol 'Decodable'
It's clearly the attributes var that is tripping up my build, but I'm not sure how I can get the desired results. Any idea how I can code my struct to support this?
Since the comments already point out that Any type has nothing to do with generics, let me jump straight into the solution.
First thing you need is some kind of wrapper type for your Any attribute values. Enums with associated values are great for that job. Since you know best what types are to be expected as an attribute, feel free to add/remove any case from my sample implementation.
enum MyAttrubuteValue {
case string(String)
case date(Date)
case data(Data)
case bool(Bool)
case double(Double)
case int(Int)
case float(Float)
}
We will be later wrapping attribute values from the [String: Any] dictionary into the wrapper enum cases, but first we need to make the type conform to the Codable protocols. I am using singleValueContainer() for the decoding/encoding so the final json will produce a regular json dicts.
extension MyAttrubuteValue: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let string = try? container.decode(String.self) {
self = .string(string)
} else if let date = try? container.decode(Date.self) {
self = .date(date)
} else if let data = try? container.decode(Data.self) {
self = .data(data)
} else if let bool = try? container.decode(Bool.self) {
self = .bool(bool)
} else if let double = try? container.decode(Double.self) {
self = .double(double)
} else if let int = try? container.decode(Int.self) {
self = .int(int)
} else if let float = try? container.decode(Float.self) {
self = .float(float)
} else {
fatalError()
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .string(let string):
try? container.encode(string)
case .date(let date):
try? container.encode(date)
case .data(let data):
try? container.encode(data)
case .bool(let bool):
try? container.encode(bool)
case .double(let double):
try? container.encode(double)
case .int(let int):
try? container.encode(int)
case .float(let float):
try? container.encode(float)
}
}
}
At this point we are good to go, but before we will decode/encode the attributes, we can use some extra interoperability between [String: Any] and [String: MyAttrubuteValue] types. To map easily between Any and MyAttrubuteValue lets add the following:
extension MyAttrubuteValue {
var value: Any {
switch self {
case .string(let value):
return value
case .date(let value):
return value
case .data(let value):
return value
case .bool(let value):
return value
case .double(let value):
return value
case .int(let value):
return value
case .float(let value):
return value
}
}
init?(_ value: Any) {
if let string = value as? String {
self = .string(string)
} else if let date = value as? Date {
self = .date(date)
} else if let data = value as? Data {
self = .data(data)
} else if let bool = value as? Bool {
self = .bool(bool)
} else if let double = value as? Double {
self = .double(double)
} else if let int = value as? Int {
self = .int(int)
} else if let float = value as? Float {
self = .float(float)
} else {
return nil
}
}
}
Now, with the quick value access and new init, we can map values easily. We are also making sure that the helper properties are only available for the dictionaries of concrete types, the ones we are working with.
extension Dictionary where Key == String, Value == Any {
var encodable: [Key: MyAttrubuteValue] {
compactMapValues(MyAttrubuteValue.init)
}
}
extension Dictionary where Key == String, Value == MyAttrubuteValue {
var any: [Key: Any] {
mapValues(\.value)
}
}
Now the final part, a custom Codable implementation for MyStruct
extension MyStruct: Codable {
enum CodingKeys: String, CodingKey {
case id = "id"
case name = "name"
case createdDate = "createdDate"
case attributes = "attributes"
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(name, forKey: .name)
try container.encode(createdDate, forKey: .createdDate)
try container.encode(attributes.encodable, forKey: .attributes)
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: .id)
name = try container.decode(String.self, forKey: .name)
createdDate = try container.decode(Date.self, forKey: .createdDate)
attributes = try container.decode(
[String: MyAttrubuteValue].self, forKey: .attributes
).any
}
}
This solution is fairly long, but pretty straight-forward at the same time. We lose automatic Codable implementation, but we got exactly what we wanted. Now you are able to encode ~Any~ type that already conforms to Codable easily, by adding an extra case to your new MyAttrubuteValue enum. One final thing to say is that we use similar approach to this one in production, and we have been happy so far.
That's a lot of code, here is a gist.
We have some response returned by backend:
{
"name": "Some name",
"number": 42,
............
"param0": value0,
"param1": value1,
"param2": value2
}
Model structure for response:
struct Model: Codable {
let name: String
let number: Int
let params: [String: Any]
}
How to make JSONDecoder combine all unknown key-value pairs into params property?
Decodable is incredibly powerful. It can decode completely arbitrary JSON, so this is just a sub-set of that problem. For a fully worked-out JSON Decodable, see this JSON.
I'll pull the concept of Key from example, but for simplicity I'll assume that values must be either Int or String. You could make parameters be [String: JSON] and use my JSON decoder instead.
struct Model: Decodable {
let name: String
let number: Int
let params: [String: Any]
// An arbitrary-string Key, with a few "well known and required" keys
struct Key: CodingKey, Equatable {
static let name = Key("name")
static let number = Key("number")
static let knownKeys = [Key.name, .number]
static func ==(lhs: Key, rhs: Key) -> Bool {
return lhs.stringValue == rhs.stringValue
}
let stringValue: String
init(_ string: String) { self.stringValue = string }
init?(stringValue: String) { self.init(stringValue) }
var intValue: Int? { return nil }
init?(intValue: Int) { return nil }
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Key.self)
// First decode what we know
name = try container.decode(String.self, forKey: .name)
number = try container.decode(Int.self, forKey:. number)
// Find all the "other" keys
let optionalKeys = container.allKeys
.filter { !Key.knownKeys.contains($0) }
// Walk through the keys and try to decode them in every legal way
// Throw an error if none of the decodes work. For this simple example
// I'm assuming it is a String or Int, but this is also solvable for
// arbitarily complex data (it's just more complicated)
// This code is uglier than it should be because of the `Any` result.
// It could be a lot nicer if parameters were a more restricted type
var p: [String: Any] = [:]
for key in optionalKeys {
if let stringValue = try? container.decode(String.self, forKey: key) {
p[key.stringValue] = stringValue
} else {
p[key.stringValue] = try container.decode(Int.self, forKey: key)
}
}
params = p
}
}
let json = Data("""
{
"name": "Some name",
"number": 42,
"param0": 1,
"param1": "2",
"param2": 3
}
""".utf8)
try JSONDecoder().decode(Model.self, from: json)
// Model(name: "Some name", number: 42, params: ["param0": 1, "param1": "2", "param2": 3])
ADDITIONAL THOUGHTS
I think the comments below are really important and future readers should look them over. I wanted to show how little code duplication is required, and how much of this can be easily extracted and reused, such that no magic or dynamic features are required.
First, extract the pieces that are common and reusable:
func additionalParameters<Key>(from container: KeyedDecodingContainer<Key>,
excludingKeys: [Key]) throws -> [String: Any]
where Key: CodingKey {
// Find all the "other" keys and convert them to Keys
let excludingKeyStrings = excludingKeys.map { $0.stringValue }
let optionalKeys = container.allKeys
.filter { !excludingKeyStrings.contains($0.stringValue)}
var p: [String: Any] = [:]
for key in optionalKeys {
if let stringValue = try? container.decode(String.self, forKey: key) {
p[key.stringValue] = stringValue
} else {
p[key.stringValue] = try container.decode(Int.self, forKey: key)
}
}
return p
}
struct StringKey: CodingKey {
let stringValue: String
init(_ string: String) { self.stringValue = string }
init?(stringValue: String) { self.init(stringValue) }
var intValue: Int? { return nil }
init?(intValue: Int) { return nil }
}
Now, the decoder for Model is reduced to this
struct Model: Decodable {
let name: String
let number: Int
let params: [String: Any]
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StringKey.self)
name = try container.decode(String.self, forKey: StringKey("name"))
number = try container.decode(Int.self, forKey: StringKey("number"))
params = try additionalParameters(from: container,
excludingKeys: ["name", "number"].map(StringKey.init))
}
}
It would be nice if there were some magic way to say "please take care of these properties in the default way," but I don't quite know what that would look like frankly. The amount of code here is about the same as for implementing NSCoding, and much less than for implementing against NSJSONSerialization, and is easily handed to swiftgen if it were too tedious (it's basically the code you have to write for init). In exchange, we get full compile-time type checking, so we know it won't crash when we get something unexpected.
There are a few ways to make even the above a bit shorter (and I'm currently thinking about ideas involving KeyPaths to make it even more convenient). The point is that the current tools are very powerful and worth exploring.
Maybe someone in the community has had similar struggles and have come up with a workable solution.
We're currently working on a polyglot key/value store. Given this, we'll generally have no knowledge of what will be stored ahead of time.
Consider the following struct
struct Character : Codable, Equatable {
let name: String
let age: Int
let gender: Gender
let hobbies: [String]
static func ==(lhs: Character, rhs: Character) -> Bool {
return (lhs.name == rhs.name
&& lhs.age == rhs.age
&& lhs.gender == rhs.gender
&& lhs.hobbies == rhs.hobbies)
}
}
When sending/receiving Character entities over the wire, everything is fairly straight forward. The user can provide us the Type in which we can decode into.
However, we do have the ability to dynamically query the entities stored within the backend. For example, we can request the value of the 'name' property and have that returned.
This dynamism is a pain point. In addition to not knowing the type of the properties outside of the fact that they are Codable, the format that is returned can be dynamic as well.
Here's some examples of response for two different calls extracting properties:
{"value":"Bilbo"}
and
{"value":["[Ljava.lang.Object;",["Bilbo",111]]}
In some cases, it could be an equivalent of a dictionary.
Right now, I have the following structs for dealing with responses:
fileprivate struct ScalarValue<T: Decodable> : Decodable {
var value: T?
}
Using the Character example, the type passed to the decoder would be:
ScalarValue<Character>.self
However, for the single value, array, or dictionary case, I'm somewhat stuck.
I've started with something like:
fileprivate struct AnyDecodable: Decodable {
init(from decoder: Decoder) throws {
// ???
}
}
Based on the possible return types I've described above, I'm not sure if this is possible with the current API.
Thoughts?
Swift can definitely handle an arbitrary JSON decodable. This isn't the same thing as an arbitrary decodable. JSON can't encode all possible values. But this structure will decode anything that can be expressed in JSON, and from there you can explore it in a type-safe way without resorting to dangerous and awkward tools like Any.
enum JSON: Decodable, CustomStringConvertible {
var description: String {
switch self {
case .string(let string): return "\"\(string)\""
case .number(let double):
if let int = Int(exactly: double) {
return "\(int)"
} else {
return "\(double)"
}
case .object(let object):
return "\(object)"
case .array(let array):
return "\(array)"
case .bool(let bool):
return "\(bool)"
case .null:
return "null"
}
}
var isEmpty: Bool {
switch self {
case .string(let string): return string.isEmpty
case .object(let object): return object.isEmpty
case .array(let array): return array.isEmpty
case .null: return true
case .number, .bool: return false
}
}
struct Key: CodingKey, Hashable, CustomStringConvertible {
var description: String {
return stringValue
}
var hashValue: Int { return stringValue.hash }
static func ==(lhs: JSON.Key, rhs: JSON.Key) -> Bool {
return lhs.stringValue == rhs.stringValue
}
let stringValue: String
init(_ string: String) { self.stringValue = string }
init?(stringValue: String) { self.init(stringValue) }
var intValue: Int? { return nil }
init?(intValue: Int) { return nil }
}
case string(String)
case number(Double) // FIXME: Split Int and Double
case object([Key: JSON])
case array([JSON])
case bool(Bool)
case null
init(from decoder: Decoder) throws {
if let string = try? decoder.singleValueContainer().decode(String.self) { self = .string(string) }
else if let number = try? decoder.singleValueContainer().decode(Double.self) { self = .number(number) }
else if let object = try? decoder.container(keyedBy: Key.self) {
var result: [Key: JSON] = [:]
for key in object.allKeys {
result[key] = (try? object.decode(JSON.self, forKey: key)) ?? .null
}
self = .object(result)
}
else if var array = try? decoder.unkeyedContainer() {
var result: [JSON] = []
for _ in 0..<(array.count ?? 0) {
result.append(try array.decode(JSON.self))
}
self = .array(result)
}
else if let bool = try? decoder.singleValueContainer().decode(Bool.self) { self = .bool(bool) }
else {
self = .null
}
}
var objectValue: [String: JSON]? {
switch self {
case .object(let object):
let mapped: [String: JSON] = Dictionary(uniqueKeysWithValues:
object.map { (key, value) in (key.stringValue, value) })
return mapped
default: return nil
}
}
var arrayValue: [JSON]? {
switch self {
case .array(let array): return array
default: return nil
}
}
subscript(key: String) -> JSON? {
guard let jsonKey = Key(stringValue: key),
case .object(let object) = self,
let value = object[jsonKey]
else { return nil }
return value
}
var stringValue: String? {
switch self {
case .string(let string): return string
default: return nil
}
}
var doubleValue: Double? {
switch self {
case .number(let number): return number
default: return nil
}
}
var intValue: Int? {
switch self {
case .number(let number): return Int(number)
default: return nil
}
}
subscript(index: Int) -> JSON? {
switch self {
case .array(let array): return array[index]
default: return nil
}
}
var boolValue: Bool? {
switch self {
case .bool(let bool): return bool
default: return nil
}
}
}
With this, you can do things like:
let bilboJSON = """
{"value":"Bilbo"}
""".data(using: .utf8)!
let bilbo = try! JSONDecoder().decode(JSON.self, from: bilboJSON)
bilbo["value"] // "Bilbo"
let javaJSON = """
{"value":["[Ljava.lang.Object;",["Bilbo",111]]}
""".data(using: .utf8)!
let java = try! JSONDecoder().decode(JSON.self, from: javaJSON)
java["value"]?[1] // ["Bilbo", 111]
java["value"]?[1]?[0]?.stringValue // "Bilbo" (as a String rather than a JSON.string)
The proliferation of ? is somewhat ugly, but using throws on this doesn't really make the interface much nicer in my experiments (particularly because subscripts can't throw). Some tweaking may be advisable based on your particular use cases.
I wrote an AnyCodable struct myself for this purpose:
struct AnyCodable: Decodable {
var value: Any
struct CodingKeys: CodingKey {
var stringValue: String
var intValue: Int?
init?(intValue: Int) {
self.stringValue = "\(intValue)"
self.intValue = intValue
}
init?(stringValue: String) { self.stringValue = stringValue }
}
init(value: Any) {
self.value = value
}
init(from decoder: Decoder) throws {
if let container = try? decoder.container(keyedBy: CodingKeys.self) {
var result = [String: Any]()
try container.allKeys.forEach { (key) throws in
result[key.stringValue] = try container.decode(AnyCodable.self, forKey: key).value
}
value = result
} else if var container = try? decoder.unkeyedContainer() {
var result = [Any]()
while !container.isAtEnd {
result.append(try container.decode(AnyCodable.self).value)
}
value = result
} else if let container = try? decoder.singleValueContainer() {
if let intVal = try? container.decode(Int.self) {
value = intVal
} else if let doubleVal = try? container.decode(Double.self) {
value = doubleVal
} else if let boolVal = try? container.decode(Bool.self) {
value = boolVal
} else if let stringVal = try? container.decode(String.self) {
value = stringVal
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "the container contains nothing serialisable")
}
} else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Could not serialise"))
}
}
}
extension AnyCodable: Encodable {
func encode(to encoder: Encoder) throws {
if let array = value as? [Any] {
var container = encoder.unkeyedContainer()
for value in array {
let decodable = AnyCodable(value: value)
try container.encode(decodable)
}
} else if let dictionary = value as? [String: Any] {
var container = encoder.container(keyedBy: CodingKeys.self)
for (key, value) in dictionary {
let codingKey = CodingKeys(stringValue: key)!
let decodable = AnyCodable(value: value)
try container.encode(decodable, forKey: codingKey)
}
} else {
var container = encoder.singleValueContainer()
if let intVal = value as? Int {
try container.encode(intVal)
} else if let doubleVal = value as? Double {
try container.encode(doubleVal)
} else if let boolVal = value as? Bool {
try container.encode(boolVal)
} else if let stringVal = value as? String {
try container.encode(stringVal)
} else {
throw EncodingError.invalidValue(value, EncodingError.Context.init(codingPath: [], debugDescription: "The value is not encodable"))
}
}
}
}
It works with nested dictionaries/arrays too. You can try it with any json in a playground.
let decoded = try! JSONDecoder().decode(AnyCodable.self, from: jsonData)
Yes, is possible to achieve what you described via the existing Codable API, and in an elegant manner I'd say (though I might be subjective here since I'm talking about my code :) ).
Let try to figure out what is needed for this task:
First things first, you need to declare all properties as optional. This is needed as the decoder is likely to have to deal with partial responses.
struct Character: Codable {
let name: String?
let age: Int?
let hobbies: [String]?
}
Next, we need a way to figure out how to map the struct properties to the various fields from the partial JSONs. Luckily the Codable API can help us here via the CodingKeys enum:
enum CodingKeys: String, CodingKey {
case name
case age
case hobbies
}
The first tricky part is to somehow convert the CodingKeys enum into an array of strings, that we can use for the array response - {"value":["[Ljava.lang.Object;",["Bilbo",111]]}. We are in luck here, there are various sources on the internet and SO that address the question of getting all cases of an enum. My preferred solutions is the RawRepresentable extension, since CodingKey is raw representable and it's raw value is a String:
// Adds support for retrieving all enum cases. Since we refer a protocol here,
// theoretically this method can be called on other types than enum
public extension RawRepresentable {
static var enumCases: [Self] {
var caseIndex: Int = 0
return Array(AnyIterator {
defer { caseIndex += 1 }
return withUnsafePointer(to: &caseIndex) {
$0.withMemoryRebound(to: Self.self, capacity: 1) { $0.pointee }
}
})
}
}
We're almost there, but we need some more work before we can decode.
Now that we have a Decodable type, a list of coding keys to use, we need a decoder that makes use of these. But before that, we need to be able to recognise types that can be partially decoded. Let's add a new protocol
protocol PartiallyDecodable: Decodable {
associatedtype PartialKeys: RawRepresentable
}
and make Character conform to it
struct Character : Codable, PartiallyDecodable {
typealias PartialKeys = CodingKeys
The finishing piece is the decoding part. We can reuse the JSONDecoder that comes with the standard library:
// Tells the form of data the server sent and we want to decode:
enum PartialDecodingStrategy {
case singleKey(String)
case arrayOfValues
case dictionary
}
extension JSONDecoder {
// Decodes an object by using a decoding strategy
func partialDecode<T>(_ type: T.Type, withStrategy strategy: PartialDecodingStrategy, from data: Data) throws -> T where T : PartiallyDecodable, T.PartialKeys.RawValue == String {
Connecting all of the above results in the following infrastructure:
// Adds support for retrieving all enum cases. Since we refer a protocol here,
// theoretically this method can be called on other types than enum
public extension RawRepresentable {
static var enumCases: [Self] {
var caseIndex: Int = 0
return Array(AnyIterator {
defer { caseIndex += 1 }
return withUnsafePointer(to: &caseIndex) {
$0.withMemoryRebound(to: Self.self, capacity: 1) { $0.pointee }
}
})
}
}
protocol PartiallyDecodable: Decodable {
associatedtype PartialKeys: RawRepresentable
}
// Tells the form of data the server sent and we want to decode:
enum PartialDecodingStrategy {
case singleKey(String)
case arrayOfValues
case dictionary
}
extension JSONDecoder {
// Decodes an object by using a decoding strategy
func partialDecode<T>(_ type: T.Type, withStrategy strategy: PartialDecodingStrategy, from data: Data) throws -> T where T : PartiallyDecodable, T.PartialKeys.RawValue == String {
guard let partialJSON = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [AnyHashable:Any] else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Invalid JSON"))
}
guard let value = partialJSON["value"] else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Missing \"value\" key"))
}
let processedJSON: [AnyHashable:Any]
switch strategy {
case let .singleKey(key):
processedJSON = [key:value]
case .arrayOfValues:
guard let values = value as? [Any],
values.count == 2,
let properties = values[1] as? [Any] else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Invalid JSON: expected a 2 elements array for the \"value\" key"))
}
processedJSON = zip(T.PartialKeys.enumCases, properties)
.reduce(into: [:]) { $0[$1.0.rawValue] = $1.1 }
case .dictionary:
guard let dict = value as? [AnyHashable:Any] else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "Invalid JSON: expected a dictionary for the \"value\" key"))
}
processedJSON = dict
}
return try decode(type, from: JSONSerialization.data(withJSONObject: processedJSON, options: []))
}
}
We want to be able to partially decode Character, so we make it adopt all the required protocols:
struct Character: Codable, PartiallyDecodable {
typealias PartialKeys = CodingKeys
let name: String?
let age: Int?
let hobbies: [String]?
enum CodingKeys: String, CodingKey {
case name
case age
case hobbies
}
}
Now the fun part, let's test it:
let decoder = JSONDecoder()
let jsonData1 = "{\"value\":\"Bilbo\"}".data(using: .utf8)!
print((try? decoder.partialDecode(Character.self,
withStrategy: .singleKey(Character.CodingKeys.name.rawValue),
from: jsonData1)) as Any)
let jsonData2 = "{\"value\":[\"[Ljava.lang.Object;\",[\"Bilbo\",111]]}".data(using: .utf8)!
print((try? decoder.partialDecode(Character.self,
withStrategy: .arrayOfValues,
from: jsonData2)) as Any)
let jsonData3 = "{\"value\":{\"name\":\"Bilbo\",\"age\":111,\"hobbies\":[\"rings\"]}}".data(using: .utf8)!
print((try? decoder.partialDecode(Character.self,
withStrategy: .dictionary,
from: jsonData3)) as Any)
As we might expect, the output is the following:
Optional(MyApp.Character(name: Optional("Bilbo"), age: nil, hobbies: nil))
Optional(MyApp.Character(name: Optional("Bilbo"), age: Optional(111), hobbies: nil))
Optional(MyApp.Character(name: Optional("Bilbo"), age: Optional(111), hobbies: Optional(["rings"])))
As we can see, with the proper infrastructure laid out, the only requirements for a type to be partially decodable is to conform to PartiallyDecodable and to have an enum that says which keys to decode. These requirements are easy to be followed.
With Swift 4's Codable protocol there's a great level of under the hood date and data conversion strategies.
Given the JSON:
{
"name": "Bob",
"age": 25,
"tax_rate": "4.25"
}
I want to coerce it into the following structure
struct ExampleJson: Decodable {
var name: String
var age: Int
var taxRate: Float
enum CodingKeys: String, CodingKey {
case name, age
case taxRate = "tax_rate"
}
}
The Date Decoding Strategy can convert a String based date into a Date.
Is there something that does that with a String based Float
Otherwise I've been stuck with using CodingKey to bring in a String and use a computing get:
enum CodingKeys: String, CodingKey {
case name, age
case sTaxRate = "tax_rate"
}
var sTaxRate: String
var taxRate: Float { return Float(sTaxRate) ?? 0.0 }
This sort of strands me doing more maintenance than it seems should be needed.
Is this the simplest manner or is there something similar to DateDecodingStrategy for other type conversions?
Update: I should note: I've also gone the route of overriding
init(from decoder:Decoder)
But that is in the opposite direction as it forces me to do it all for myself.
Using Swift 5.1, you may choose one of the three following ways in order to solve your problem.
#1. Using Decodable init(from:) initializer
Use this strategy when you need to convert from String to Float for a single struct, enum or class.
import Foundation
struct ExampleJson: Decodable {
var name: String
var age: Int
var taxRate: Float
enum CodingKeys: String, CodingKey {
case name, age, taxRate = "tax_rate"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: CodingKeys.name)
age = try container.decode(Int.self, forKey: CodingKeys.age)
let taxRateString = try container.decode(String.self, forKey: CodingKeys.taxRate)
guard let taxRateFloat = Float(taxRateString) else {
let context = DecodingError.Context(codingPath: container.codingPath + [CodingKeys.taxRate], debugDescription: "Could not parse json key to a Float object")
throw DecodingError.dataCorrupted(context)
}
taxRate = taxRateFloat
}
}
Usage:
import Foundation
let jsonString = """
{
"name": "Bob",
"age": 25,
"tax_rate": "4.25"
}
"""
let data = jsonString.data(using: String.Encoding.utf8)!
let decoder = JSONDecoder()
let exampleJson = try! decoder.decode(ExampleJson.self, from: data)
dump(exampleJson)
/*
prints:
▿ __lldb_expr_126.ExampleJson
- name: "Bob"
- age: 25
- taxRate: 4.25
*/
#2. Using an intermediate model
Use this strategy when you have many nested keys in your JSON or when you need to convert many keys (e.g. from String to Float) from your JSON.
import Foundation
fileprivate struct PrivateExampleJson: Decodable {
var name: String
var age: Int
var taxRate: String
enum CodingKeys: String, CodingKey {
case name, age, taxRate = "tax_rate"
}
}
struct ExampleJson: Decodable {
var name: String
var age: Int
var taxRate: Float
init(from decoder: Decoder) throws {
let privateExampleJson = try PrivateExampleJson(from: decoder)
name = privateExampleJson.name
age = privateExampleJson.age
guard let convertedTaxRate = Float(privateExampleJson.taxRate) else {
let context = DecodingError.Context(codingPath: [], debugDescription: "Could not parse json key to a Float object")
throw DecodingError.dataCorrupted(context)
}
taxRate = convertedTaxRate
}
}
Usage:
import Foundation
let jsonString = """
{
"name": "Bob",
"age": 25,
"tax_rate": "4.25"
}
"""
let data = jsonString.data(using: String.Encoding.utf8)!
let decoder = JSONDecoder()
let exampleJson = try! decoder.decode(ExampleJson.self, from: data)
dump(exampleJson)
/*
prints:
▿ __lldb_expr_126.ExampleJson
- name: "Bob"
- age: 25
- taxRate: 4.25
*/
#3. Using a KeyedDecodingContainer extension method
Use this strategy when converting from some JSON keys' types to your model's property types (e.g. String to Float) is a common pattern in your application.
import Foundation
extension KeyedDecodingContainer {
func decode(_ type: Float.Type, forKey key: Key) throws -> Float {
if let stringValue = try? self.decode(String.self, forKey: key) {
guard let floatValue = Float(stringValue) else {
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Could not parse json key to a Float object")
throw DecodingError.dataCorrupted(context)
}
return floatValue
} else {
let doubleValue = try self.decode(Double.self, forKey: key)
return Float(doubleValue)
}
}
}
struct ExampleJson: Decodable {
var name: String
var age: Int
var taxRate: Float
enum CodingKeys: String, CodingKey {
case name, age, taxRate = "tax_rate"
}
}
Usage:
import Foundation
let jsonString = """
{
"name": "Bob",
"age": 25,
"tax_rate": "4.25"
}
"""
let data = jsonString.data(using: String.Encoding.utf8)!
let decoder = JSONDecoder()
let exampleJson = try! decoder.decode(ExampleJson.self, from: data)
dump(exampleJson)
/*
prints:
▿ __lldb_expr_126.ExampleJson
- name: "Bob"
- age: 25
- taxRate: 4.25
*/
Unfortunately, I don't believe such an option exists in the current JSONDecoder API. There only exists an option in order to convert exceptional floating-point values to and from a string representation.
Another possible solution to decoding manually is to define a Codable wrapper type for any LosslessStringConvertible that can encode to and decode from its String representation:
struct StringCodableMap<Decoded : LosslessStringConvertible> : Codable {
var decoded: Decoded
init(_ decoded: Decoded) {
self.decoded = decoded
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let decodedString = try container.decode(String.self)
guard let decoded = Decoded(decodedString) else {
throw DecodingError.dataCorruptedError(
in: container, debugDescription: """
The string \(decodedString) is not representable as a \(Decoded.self)
"""
)
}
self.decoded = decoded
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(decoded.description)
}
}
Then you can just have a property of this type and use the auto-generated Codable conformance:
struct Example : Codable {
var name: String
var age: Int
var taxRate: StringCodableMap<Float>
private enum CodingKeys: String, CodingKey {
case name, age
case taxRate = "tax_rate"
}
}
Although unfortunately, now you have to talk in terms of taxRate.decoded in order to interact with the Float value.
However you could always define a simple forwarding computed property in order to alleviate this:
struct Example : Codable {
var name: String
var age: Int
private var _taxRate: StringCodableMap<Float>
var taxRate: Float {
get { return _taxRate.decoded }
set { _taxRate.decoded = newValue }
}
private enum CodingKeys: String, CodingKey {
case name, age
case _taxRate = "tax_rate"
}
}
Although this still isn't as a slick as it really should be – hopefully a later version of the JSONDecoder API will include more custom decoding options, or else have the ability to express type conversions within the Codable API itself.
However one advantage of creating the wrapper type is that it can also be used in order to make manual decoding and encoding simpler. For example, with manual decoding:
struct Example : Decodable {
var name: String
var age: Int
var taxRate: Float
private enum CodingKeys: String, CodingKey {
case name, age
case taxRate = "tax_rate"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
self.age = try container.decode(Int.self, forKey: .age)
self.taxRate = try container.decode(StringCodableMap<Float>.self,
forKey: .taxRate).decoded
}
}
You can always decode manually. So, given:
{
"name": "Bob",
"age": 25,
"tax_rate": "4.25"
}
You can do:
struct Example: Codable {
let name: String
let age: Int
let taxRate: Float
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decode(String.self, forKey: .name)
age = try values.decode(Int.self, forKey: .age)
guard let rate = try Float(values.decode(String.self, forKey: .taxRate)) else {
throw DecodingError.dataCorrupted(.init(codingPath: [CodingKeys.taxRate], debugDescription: "Expecting string representation of Float"))
}
taxRate = rate
}
enum CodingKeys: String, CodingKey {
case name, age
case taxRate = "tax_rate"
}
}
See Encode and Decode Manually in Encoding and Decoding Custom Types.
But I agree, that it seems like there should be a more elegant string conversion process equivalent to DateDecodingStrategy given how many JSON sources out there incorrectly return numeric values as strings.
I know that this is a really late answer, but I started working on Codable couple of days back only. And I bumped into a similar issue.
In order to convert the string to floating number, you can write an extension to KeyedDecodingContainer and call the method in the extension from init(from decoder: Decoder){}
For the problem mentioned in this issue, see the extension I wrote below;
extension KeyedDecodingContainer {
func decodeIfPresent(_ type: Float.Type, forKey key: K, transformFrom: String.Type) throws -> Float? {
guard let value = try decodeIfPresent(transformFrom, forKey: key) else {
return nil
}
return Float(value)
}
func decode(_ type: Float.Type, forKey key: K, transformFrom: String.Type) throws -> Float {
guard let valueAsString = try? decode(transformFrom, forKey: key),
let value = Float(valueAsString) else {
throw DecodingError.typeMismatch(
type,
DecodingError.Context(
codingPath: codingPath,
debugDescription: "Decoding of \(type) from \(transformFrom) failed"
)
)
}
return value
}
}
You can call this method from init(from decoder: Decoder) method. See an example below;
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
taxRate = try container.decodeIfPresent(Float.self, forKey: .taxRate, transformFrom: String.self)
}
In fact, you can use this approach to convert any type of data to any other type. You can convert string to Date, string to bool, string to float, float to int etc.
Actually to convert a string to Date object, I will prefer this approach over JSONEncoder().dateEncodingStrategy because if you write it properly, you can include different date formats in the same response.
Hope I helped.
Updated the decode method to return non-optional on suggestion from #Neil.
I used Suran's version, but updated it to return non-optional value for decode(). To me this is the most elegant version. Swift 5.2.
extension KeyedDecodingContainer {
func decodeIfPresent(_ type: Float.Type, forKey key: K, transformFrom: String.Type) throws -> Float? {
guard let value = try decodeIfPresent(transformFrom, forKey: key) else {
return nil
}
return Float(value)
}
func decode(_ type: Float.Type, forKey key: K, transformFrom: String.Type) throws -> Float {
guard let str = try? decode(transformFrom, forKey: key),
let value = Float(str) else {
throw DecodingError.typeMismatch(Int.self, DecodingError.Context(codingPath: codingPath, debugDescription: "Decoding of \(type) from \(transformFrom) failed"))
}
return value
}
}
You can use lazy var to convert the property to another type:
struct ExampleJson: Decodable {
var name: String
var age: Int
lazy var taxRate: Float = {
Float(self.tax_rate)!
}()
private var tax_rate: String
}
One disadvantage of this approach is that you cannot define a let constant if you want to access taxRate, since the first time you access it, you are mutating the struct.
// Cannot use `let` here
var example = try! JSONDecoder().decode(ExampleJson.self, from: data)
The options above only deal with the situation that the given field is always String. Many times I've met APIs where the output was once a string, other times number. So this is my suggestion to solve this. It is up to you to alter this to throw exception or set the decoded value to nil.
var json = """
{
"title": "Apple",
"id": "20"
}
""";
var jsonWithInt = """
{
"title": "Apple",
"id": 20
}
""";
struct DecodableNumberFromStringToo<T: LosslessStringConvertible & Decodable & Numeric>: Decodable {
var value: T
init(from decoder: Decoder) {
print("Decoding")
if let container = try? decoder.singleValueContainer() {
if let val = try? container.decode(T.self) {
value = val
return
}
if let str = try? container.decode(String.self) {
value = T.init(str) ?? T.zero
return
}
}
value = T.zero
}
}
struct MyData: Decodable {
let title: String
let _id: DecodableNumberFromStringToo<Int>
enum CodingKeys: String, CodingKey {
case title, _id = "id"
}
var id: Int {
return _id.value
}
}
do {
let parsedJson = try JSONDecoder().decode(MyData.self, from: json.data(using: .utf8)!)
print(parsedJson.id)
} catch {
print(error as? DecodingError)
}
do {
let parsedJson = try JSONDecoder().decode(MyData.self, from: jsonWithInt.data(using: .utf8)!)
print(parsedJson.id)
} catch {
print(error as? DecodingError)
}
How to used JSONDecodable in Swift 4:
Get the JSON Response and Create Struct
Conform Decodable class in Struct
Other steps in this GitHub project, a simple example