Swift json dynamic key parsing for json - json

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)
}

Related

Decode JSON with variables in Swift

I am trying to decode this type of JSON-Data in Swift
{"Total ingredients":[{"PE-LLD":"54.4 %"},{"PE-HD":"41.1 %"},{"TiO2":"4.5 %"}]}
The name and number of ingredients is variable. Therefore I am only able to decode it in this type of structure:
struct Product: Codable {
var total_ingredients: [[String: String]]?
private enum CodingKeys : String, CodingKey {
case total_ingredients = "Total ingredients"
}
}
But I would like to be able to decode it in either one dictionary: var total_ingredients: [String: String]? or my preferred choice in an array of objects: var total_ingredients: [Ingredient]?
struct Ingredient: Codable {
var name: String
var percentage: String
}
I already tried to solve my problem with an extension but it isn't working and I don't think that's the correct approach:
extension Ingredient {
init(_ ingredient: [String: String]) {
var key: String = ""
var value: String = ""
for data in ingredient {
key = data.key
value = data.value
}
self = .init(name: key, percentage: value)
}
}
Thanks in advance :)
You have to implement init(from decoder and map the array of dictionaries to Ingredient instances
struct Product: Decodable {
let totalIngredients: [Ingredient]
private enum CodingKeys : String, CodingKey { case totalIngredients = "Total ingredients" }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let ingredientData = try container.decode([[String:String]].self, forKey: .totalIngredients)
totalIngredients = ingredientData.compactMap({ dict -> Ingredient? in
guard let key = dict.keys.first, let value = dict[key] else { return nil }
return Ingredient(name: key, percentage: value)
})
}
}
struct Ingredient {
let name, percentage: String
}
let jsonString = """
{"Total ingredients":[{"PE-LLD":"54.4 %"},{"PE-HD":"41.1 %"},{"TiO2":"4.5 %"}]}
"""
let data = Data(jsonString.utf8)
do {
let result = try JSONDecoder().decode(Product.self, from: data)
print(result)
} catch {
print(error)
}
The extension is not needed.

How to detect if data value is nil, when struct properties are optional

Parsing data using Codable don't fail.
when I use generic I want the parsing to fail if the fields are deferent then my object fields
struct someStruct: Codable {
var name: String?
var age: Int?
}
JSON :
{
"some_key": 123
}
You should use Codable to parse data whether it is nil or not. And after that you can check Struct like below for nil value-
if let name = yourStruct.name as? String {
} else {
//nil
}
You can just use Codable with your struct and when you parse the JSON data, and since your properties in your struct are optional, you can safe unwrap the values using if-let . I Have provided a sample example for the same
import Foundation
let jsonData = """
{
"some_key": 123
}
"""
let data = Data(jsonData.utf8)
struct someStruct: Codable {
var name: String?
var age: Int?
}
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(someStruct.self, from: data)
// Either use this
print(decodedData.name ?? "Name not specified")
// Or use this
if let name = decodedData.name {
// Name is not nil
// Sample Example
print(name)
}
else {
// Name is nil , Handle the situation accordingly
// Sample Example
print("Name not specified")
}
} catch {
print("Could not parse JSON Data")
}
So you want to throw if a field is missing, but continue when the field is specified but nil.
You need to implement custom encoding to solve that:
enum EncodingError: Error {
case missing
}
struct Struct: Codable {
let age: Int
let name: String?
enum CodingKeys: CodingKey {
case age
case name
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
// normal decoding
age = try container.decode(Int.self, forKey: .age)
// check if key exists or throw
guard container.contains(.name) else {
throw EncodingError.missing
}
name = try container.decode(String?.self, forKey: .name)
}
}
let correctData = """
{
"age": 34,
"name": null
}
""".data(using: .utf8)!
let failData = """
{
"age": 33
}
""".data(using: .utf8)!
do {
let decoder = JSONDecoder()
let encoded = try decoder.decode(Struct.self, from: correctData) // succeeds
let fail = try decoder.decode(Struct.self, from: failData) // fails
} catch(let error) where error is EncodingError {
error
} catch {
error
}

Swift, How to Parse/Decode the JSON using Decodable and Codable, When key are unknow/dynamic

Below is my JSON, and I am not able to decode(using CodingKeys)
The data within the regions key is a Dictionary ("IN-WB", "IN-DL" & so on....), as the keys are dynamic, it can be changed more or less.
Please help me parsing the same using Decodable and Codable.
All the data should be within the single model.
{
"provider_code": "AIIN",
"name": "Jio India",
"regions": [
{
"IN-WB": "West Bengal"
},
{
"IN-DL": "Delhi NCR"
},
{
"IN-TN": "Tamil Nadu"
},
{
"IN": "India"
}
]
}
Just use a Dictionary for the regions.
struct Locations: Codable {
let providerCode: String
let name: String
let regions: [[String: String]]
enum CodingKeys: String, CodingKey {
case providerCode = "provider_code"
case name, regions
}
}
You cannot create a specific model for the regions as you wont know the property names
One of possible approach, without using dictionary. But still we have to found key at first )
I like this style as we can use Regions from beginning.
// example data.
let string = "{\"provider_code\":\"AIIN\",\"name\":\"Jio India\",\"regions\":[{\"IN-WB\":\"West Bengal\"},{\"IN-DL\":\"Delhi NCR\"},{\"IN-TN\":\"Tamil Nadu\"},{\"IN\":\"India\"}]}"
let data = string.data(using: .utf8)!
// little helper
struct DynamicGlobalKey: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int? { return nil }
init?(intValue: Int) { return nil }
}
// model
struct Location: Decodable {
let providerCode: String
let name: String
let regions: [Region]
}
extension Location {
struct Region: Decodable {
let key: String
let name: String
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: DynamicGlobalKey.self)
key = container.allKeys.first!.stringValue
name = try container.decode(String.self, forKey: container.allKeys.first!)
}
}
}
// example of decoding.
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let location = try decoder.decode(Location.self, from: data)

Swift 4 JSON Decodable simplest way to decode type change

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

How to decode a nested JSON struct with Swift Decodable protocol?

Here is my JSON
{
"id": 1,
"user": {
"user_name": "Tester",
"real_info": {
"full_name":"Jon Doe"
}
},
"reviews_count": [
{
"count": 4
}
]
}
Here is the structure I want it saved to (incomplete)
struct ServerResponse: Decodable {
var id: String
var username: String
var fullName: String
var reviewCount: Int
enum CodingKeys: String, CodingKey {
case id,
// How do i get nested values?
}
}
I have looked at Apple's Documentation on decoding nested structs, but I still do not understand how to do the different levels of the JSON properly. Any help will be much appreciated.
Another approach is to create an intermediate model that closely matches the JSON (with the help of a tool like quicktype.io), let Swift generate the methods to decode it, and then pick off the pieces that you want in your final data model:
// snake_case to match the JSON and hence no need to write CodingKey enums
fileprivate struct RawServerResponse: Decodable {
struct User: Decodable {
var user_name: String
var real_info: UserRealInfo
}
struct UserRealInfo: Decodable {
var full_name: String
}
struct Review: Decodable {
var count: Int
}
var id: Int
var user: User
var reviews_count: [Review]
}
struct ServerResponse: Decodable {
var id: String
var username: String
var fullName: String
var reviewCount: Int
init(from decoder: Decoder) throws {
let rawResponse = try RawServerResponse(from: decoder)
// Now you can pick items that are important to your data model,
// conveniently decoded into a Swift structure
id = String(rawResponse.id)
username = rawResponse.user.user_name
fullName = rawResponse.user.real_info.full_name
reviewCount = rawResponse.reviews_count.first!.count
}
}
This also allows you to easily iterate through reviews_count, should it contain more than 1 value in the future.
In order to solve your problem, you can split your RawServerResponse implementation into several logic parts (using Swift 5).
#1. Implement the properties and required coding keys
import Foundation
struct RawServerResponse {
enum RootKeys: String, CodingKey {
case id, user, reviewCount = "reviews_count"
}
enum UserKeys: String, CodingKey {
case userName = "user_name", realInfo = "real_info"
}
enum RealInfoKeys: String, CodingKey {
case fullName = "full_name"
}
enum ReviewCountKeys: String, CodingKey {
case count
}
let id: Int
let userName: String
let fullName: String
let reviewCount: Int
}
#2. Set the decoding strategy for id property
extension RawServerResponse: Decodable {
init(from decoder: Decoder) throws {
// id
let container = try decoder.container(keyedBy: RootKeys.self)
id = try container.decode(Int.self, forKey: .id)
/* ... */
}
}
#3. Set the decoding strategy for userName property
extension RawServerResponse: Decodable {
init(from decoder: Decoder) throws {
/* ... */
// userName
let userContainer = try container.nestedContainer(keyedBy: UserKeys.self, forKey: .user)
userName = try userContainer.decode(String.self, forKey: .userName)
/* ... */
}
}
#4. Set the decoding strategy for fullName property
extension RawServerResponse: Decodable {
init(from decoder: Decoder) throws {
/* ... */
// fullName
let realInfoKeysContainer = try userContainer.nestedContainer(keyedBy: RealInfoKeys.self, forKey: .realInfo)
fullName = try realInfoKeysContainer.decode(String.self, forKey: .fullName)
/* ... */
}
}
#5. Set the decoding strategy for reviewCount property
extension RawServerResponse: Decodable {
init(from decoder: Decoder) throws {
/* ...*/
// reviewCount
var reviewUnkeyedContainer = try container.nestedUnkeyedContainer(forKey: .reviewCount)
var reviewCountArray = [Int]()
while !reviewUnkeyedContainer.isAtEnd {
let reviewCountContainer = try reviewUnkeyedContainer.nestedContainer(keyedBy: ReviewCountKeys.self)
reviewCountArray.append(try reviewCountContainer.decode(Int.self, forKey: .count))
}
guard let reviewCount = reviewCountArray.first else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: container.codingPath + [RootKeys.reviewCount], debugDescription: "reviews_count cannot be empty"))
}
self.reviewCount = reviewCount
}
}
Complete implementation
import Foundation
struct RawServerResponse {
enum RootKeys: String, CodingKey {
case id, user, reviewCount = "reviews_count"
}
enum UserKeys: String, CodingKey {
case userName = "user_name", realInfo = "real_info"
}
enum RealInfoKeys: String, CodingKey {
case fullName = "full_name"
}
enum ReviewCountKeys: String, CodingKey {
case count
}
let id: Int
let userName: String
let fullName: String
let reviewCount: Int
}
extension RawServerResponse: Decodable {
init(from decoder: Decoder) throws {
// id
let container = try decoder.container(keyedBy: RootKeys.self)
id = try container.decode(Int.self, forKey: .id)
// userName
let userContainer = try container.nestedContainer(keyedBy: UserKeys.self, forKey: .user)
userName = try userContainer.decode(String.self, forKey: .userName)
// fullName
let realInfoKeysContainer = try userContainer.nestedContainer(keyedBy: RealInfoKeys.self, forKey: .realInfo)
fullName = try realInfoKeysContainer.decode(String.self, forKey: .fullName)
// reviewCount
var reviewUnkeyedContainer = try container.nestedUnkeyedContainer(forKey: .reviewCount)
var reviewCountArray = [Int]()
while !reviewUnkeyedContainer.isAtEnd {
let reviewCountContainer = try reviewUnkeyedContainer.nestedContainer(keyedBy: ReviewCountKeys.self)
reviewCountArray.append(try reviewCountContainer.decode(Int.self, forKey: .count))
}
guard let reviewCount = reviewCountArray.first else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: container.codingPath + [RootKeys.reviewCount], debugDescription: "reviews_count cannot be empty"))
}
self.reviewCount = reviewCount
}
}
Usage
let jsonString = """
{
"id": 1,
"user": {
"user_name": "Tester",
"real_info": {
"full_name":"Jon Doe"
}
},
"reviews_count": [
{
"count": 4
}
]
}
"""
let jsonData = jsonString.data(using: .utf8)!
let decoder = JSONDecoder()
let serverResponse = try! decoder.decode(RawServerResponse.self, from: jsonData)
dump(serverResponse)
/*
prints:
▿ RawServerResponse #1 in __lldb_expr_389
- id: 1
- user: "Tester"
- fullName: "Jon Doe"
- reviewCount: 4
*/
Rather than having one big CodingKeys enumeration with all the keys you'll need for decoding the JSON, I would advise splitting the keys up for each of your nested JSON objects, using nested enumerations to preserve the hierarchy:
// top-level JSON object keys
private enum CodingKeys : String, CodingKey {
// using camelCase case names, with snake_case raw values where necessary.
// the raw values are what's used as the actual keys for the JSON object,
// and default to the case name unless otherwise specified.
case id, user, reviewsCount = "reviews_count"
// "user" JSON object keys
enum User : String, CodingKey {
case username = "user_name", realInfo = "real_info"
// "real_info" JSON object keys
enum RealInfo : String, CodingKey {
case fullName = "full_name"
}
}
// nested JSON objects in "reviews" keys
enum ReviewsCount : String, CodingKey {
case count
}
}
This will make it easier to keep track of the keys at each level in your JSON.
Now, bearing in mind that:
A keyed container is used to decode a JSON object, and is decoded with a CodingKey conforming type (such as the ones we've defined above).
An unkeyed container is used to decode a JSON array, and is decoded sequentially (i.e each time you call a decode or nested container method on it, it advances to the next element in the array). See the second part of the answer for how you can iterate through one.
After getting your top-level keyed container from the decoder with container(keyedBy:) (as you have a JSON object at the top-level), you can repeatedly use the methods:
nestedContainer(keyedBy:forKey:) to get a nested object from an object for a given key
nestedUnkeyedContainer(forKey:) to get a nested array from an object for a given key
nestedContainer(keyedBy:) to get the next nested object from an array
nestedUnkeyedContainer() to get the next nested array from an array
For example:
struct ServerResponse : Decodable {
var id: Int, username: String, fullName: String, reviewCount: Int
private enum CodingKeys : String, CodingKey { /* see above definition in answer */ }
init(from decoder: Decoder) throws {
// top-level container
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int.self, forKey: .id)
// container for { "user_name": "Tester", "real_info": { "full_name": "Jon Doe" } }
let userContainer =
try container.nestedContainer(keyedBy: CodingKeys.User.self, forKey: .user)
self.username = try userContainer.decode(String.self, forKey: .username)
// container for { "full_name": "Jon Doe" }
let realInfoContainer =
try userContainer.nestedContainer(keyedBy: CodingKeys.User.RealInfo.self,
forKey: .realInfo)
self.fullName = try realInfoContainer.decode(String.self, forKey: .fullName)
// container for [{ "count": 4 }] – must be a var, as calling a nested container
// method on it advances it to the next element.
var reviewCountContainer =
try container.nestedUnkeyedContainer(forKey: .reviewsCount)
// container for { "count" : 4 }
// (note that we're only considering the first element of the array)
let firstReviewCountContainer =
try reviewCountContainer.nestedContainer(keyedBy: CodingKeys.ReviewsCount.self)
self.reviewCount = try firstReviewCountContainer.decode(Int.self, forKey: .count)
}
}
Example decoding:
let jsonData = """
{
"id": 1,
"user": {
"user_name": "Tester",
"real_info": {
"full_name":"Jon Doe"
}
},
"reviews_count": [
{
"count": 4
}
]
}
""".data(using: .utf8)!
do {
let response = try JSONDecoder().decode(ServerResponse.self, from: jsonData)
print(response)
} catch {
print(error)
}
// ServerResponse(id: 1, username: "Tester", fullName: "Jon Doe", reviewCount: 4)
Iterating through an unkeyed container
Considering the case where you want reviewCount to be an [Int], where each element represents the value for the "count" key in the nested JSON:
"reviews_count": [
{
"count": 4
},
{
"count": 5
}
]
You'll need to iterate through the nested unkeyed container, getting the nested keyed container at each iteration, and decoding the value for the "count" key. You can use the count property of the unkeyed container in order to pre-allocate the resultant array, and then the isAtEnd property to iterate through it.
For example:
struct ServerResponse : Decodable {
var id: Int
var username: String
var fullName: String
var reviewCounts = [Int]()
// ...
init(from decoder: Decoder) throws {
// ...
// container for [{ "count": 4 }, { "count": 5 }]
var reviewCountContainer =
try container.nestedUnkeyedContainer(forKey: .reviewsCount)
// pre-allocate the reviewCounts array if we can
if let count = reviewCountContainer.count {
self.reviewCounts.reserveCapacity(count)
}
// iterate through each of the nested keyed containers, getting the
// value for the "count" key, and appending to the array.
while !reviewCountContainer.isAtEnd {
// container for a single nested object in the array, e.g { "count": 4 }
let nestedReviewCountContainer = try reviewCountContainer.nestedContainer(
keyedBy: CodingKeys.ReviewsCount.self)
self.reviewCounts.append(
try nestedReviewCountContainer.decode(Int.self, forKey: .count)
)
}
}
}
Copy the json file to https://app.quicktype.io
Select Swift (if you use Swift 5, check the compatibility switch for Swift 5)
Use the following code to decode the file
Voila!
let file = "data.json"
guard let url = Bundle.main.url(forResource: "data", withExtension: "json") else{
fatalError("Failed to locate \(file) in bundle.")
}
guard let data = try? Data(contentsOf: url) else{
fatalError("Failed to locate \(file) in bundle.")
}
let yourObject = try? JSONDecoder().decode(YourModel.self, from: data)
Many good answers have already been posted, but there is a simpler method not described yet IMO.
When the JSON field names are written using snake_case_notation you can still use the camelCaseNotation in your Swift file.
You just need to set
decoder.keyDecodingStrategy = .convertFromSnakeCase
After this ☝️ line Swift will automatically match all the snake_case fields from the JSON to the camelCase fields in the Swift model.
E.g.
user_name` -> userName
reviews_count -> `reviewsCount
...
Here's the full code
1. Writing the Model
struct Response: Codable {
let id: Int
let user: User
let reviewsCount: [ReviewCount]
struct User: Codable {
let userName: String
struct RealInfo: Codable {
let fullName: String
}
}
struct ReviewCount: Codable {
let count: Int
}
}
2. Setting the Decoder
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
3. Decoding
do {
let response = try? decoder.decode(Response.self, from: data)
print(response)
} catch {
debugPrint(error)
}
Also you can use library KeyedCodable I prepared. It will require less code. Let me know what you think about it.
struct ServerResponse: Decodable, Keyedable {
var id: String!
var username: String!
var fullName: String!
var reviewCount: Int!
private struct ReviewsCount: Codable {
var count: Int
}
mutating func map(map: KeyMap) throws {
var id: Int!
try id <<- map["id"]
self.id = String(id)
try username <<- map["user.user_name"]
try fullName <<- map["user.real_info.full_name"]
var reviewCount: [ReviewsCount]!
try reviewCount <<- map["reviews_count"]
self.reviewCount = reviewCount[0].count
}
init(from decoder: Decoder) throws {
try KeyedDecoder(with: decoder).decode(to: &self)
}
}