Decode a PascalCase JSON with JSONDecoder - json

I need to decode a JSON with uppercased first letters (aka PascalCase or UppperCamelCase) like this :
{
"Title": "example",
"Items": [
"hello",
"world"
]
}
So I created a model conforming to Codable:
struct Model: Codable {
let title: String
let items: [String]
}
But JSONDecoder raises an error because the case is different.
Swift.DecodingError.keyNotFound(CodingKeys(stringValue: "title", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"title\", intValue: nil) (\"title\").", underlyingError: nil))
I would like to keep my model's properties in camelCase but I can't change the JSON fomat.

A nice solution I found is to create a KeyDecodingStrategy similar to the .convertFromSnakeCase available in Foundation.
extension JSONDecoder.KeyDecodingStrategy {
static var convertFromPascalCase: JSONDecoder.KeyDecodingStrategy {
return .custom { keys -> CodingKey in
// keys array is never empty
let key = keys.last!
// Do not change the key for an array
guard key.intValue == nil else {
return key
}
let codingKeyType = type(of: key)
let newStringValue = key.stringValue.firstCharLowercased()
return codingKeyType.init(stringValue: newStringValue)!
}
}
}
private extension String {
func firstCharLowercased() -> String {
prefix(1).lowercased() + dropFirst()
}
}
It can be used easily like so :
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromPascalCase
let model = try! decoder.decode(Model.self, from: json)
Full example on Gist

Related

how to parse this complex nested Json

The Json is valid but I m getting nil with below code and struct for the returned json.
problem encountered:
at JSonDecoder.decode() : it returned this error msg:
keyNotFound(CodingKeys(stringValue: "items", intValue: nil), Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "items", intValue: nil), _JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "No value associated with key CodingKeys(stringValue: "items", intValue: nil) ("items").", underlyingError: nil))
How to get these a) image b) location from the Json
Thanks
here the code
func getJsonMapData(){
guard let mapUrl = URL(string: "https://xxxxxx/traffic-images") else { return }
URLSession.shared.dataTask(with: mapUrl) { (data, response, error) in
guard error == nil else { return}
guard let data = data else { return}
//- problem:
do {
let LocationArrDict = try JSONDecoder().decode([String:[Location]].self, from: data)else{
print(LocationArrDict)
} catch {
print(error)
}
}.resume()
}
//------------- return Json String:
{
"items":[
{
"timestamp":"2020-12-05T08:45:43+08:00",
"cameras":[
{
"timestamp":"2020-11-05T08:42:43+08:00",
"image":"https://xxxxxxxxx/traffic-images/2020/12/2ab06cd8-4dcf-434c-b758-804e690e57db.jpg",
"location":{
"latitude":1.29531332,
"longitude":103.871146
},
"camera_id":"1001",
"image_metadata":{
"height":240,
"width":320,
"md5":"c9686a013f3a2ed4af61260811661fc4"
}
},
{
"timestamp":"2020-11-05T08:42:43+08:00",
"image":"https://xxxxxxxxxx/traffic-images/2020/12/9f6d307e-8b05-414d-b27d-bf1414aa2cc7.jpg",
"location":{
"latitude":1.319541067,
"longitude":103.8785627
},
"camera_id":"1002",
"image_metadata":{
"height":240,
"width":320,
"md5":"78060d8fbdd241adf43a2f1ae5d252b1"
}
},
........
{
"timestamp":"2020-12-05T08:42:43+08:00",
"image":"https://xxxxxx/traffic-images/2020/12/98f64fe6-5985-4a8a-852f-0be24b0a6271.jpg",
"location":{
"latitude":1.41270056,
"longitude":103.80642712
},
"camera_id":"9706",
"image_metadata":{
"height":360,
"width":640,
"md5":"f63d54176620fa1d9896fa438b3cc753"
}
}
]
}
],
"api_info":{
"status":"healthy"
}
}
//------------ struct for the return Json result:
// MARK: - Location
struct Location: Codable {
let items: [Item]
let apiInfo: APIInfo
enum CodingKeys: String, CodingKey {
case items
case apiInfo = "api_info"
}
}
// MARK: - APIInfo
struct APIInfo: Codable {
let status: String
}
// MARK: - Item
struct Item: Codable {
let timestamp: Date
let cameras: [Camera]
}
// MARK: - Camera
struct Camera: Codable {
let timestamp: Date
let image: String
let location: LocationClass
let cameraID: String
let imageMetadata: ImageMetadata
enum CodingKeys: String, CodingKey {
case timestamp, image, location
case cameraID = "camera_id"
case imageMetadata = "image_metadata"
}
}
// MARK: - ImageMetadata
struct ImageMetadata: Codable {
let height, width: Int
let md5: String
}
// MARK: - LocationClass
struct LocationClass: Codable {
let latitude, longitude: Double
}
``
Error #1: The type to be decoded is wrong it must be Location.self.
Error #2: To decode the ISO date as Date you have to add the .iso8601 date decoding strategy.
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let locationArrDict = try decoder.decode(Location.self, from: data)
print(locationArrDict)
} catch {
print(error)
}
And you could decode the strings representing an URL directly to URL
You have to create your data models and describe them as the response you expect to receive. I will just give you a brief example based on your json how you would decode it.
First of all in order to decode a JSON using JSONDecoder your models have to conform to Decodable protocol. Then you have to create those models.
struct Item: Decodable {
let timestamp: Date
// let cameras: [...]
}
struct ApiInfo: Decodable {
enum Status: String, Decodable {
case healthy
}
let status: Status
}
struct Response: Decodable {
let items: [Item]
let apiInfo: ApiInfo
}
Then you have to configure your JSONDecoder and decode that JSON. As we can see clearly, your JSON uses snake_case naming convention and that is not the default one for JSONDecoder so you have to set it. Same also applies for the dates - it is used iso8601 standard of representation.
let jsonDecoder = JSONDecoder()
jsonDecoder.dateDecodingStrategy = .iso8601
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
Finally, you have you decode it.
do {
let response = try jsonDecoder.decode(Response.self, from: jsonData)
print(response)
} catch {
print(error)
}

How to decode JSON object with any number of key–value pairs in Swift?

I’m using Swift to try and decode JSON:API-formatted JSON results. The JSON I’m trying to parse has a shape like this:
{
"data": {
"type": "video",
"id": "1",
"attributes": {
"name": "Test Video",
"duration": 1234
}
}
}
I’m trying to create a Swift struct that will encode these JSON objects, but I’m having issues with the attributes key as it could contain any number of attributes.
The Swift structs I’m trying to encode the above into look like this:
struct JSONAPIMultipleResourceResponse: Decodable {
var data: [JSONAPIResource]
}
struct JSONAPIResource: Decodable {
var type: String
var id: String
var attributes: [String, String]?
}
The type and id attributes should be present in every JSON:API result. The attributes key should be a list of any number of key–value pairs; both the key and value should be strings.
I’m then trying to decode a JSON response from an API like this:
let response = try! JSONDecoder().decode(JSONAPIMultipleResourceResponse.self, from: data!)
The above works if I leave the type and id properties in my JSONAPIResource Swift struct, but as soon as I try and do anything with attributes I get the following error:
Fatal error: 'try!' expression unexpectedly raised an error: Swift.DecodingError.valueNotFound(Swift.String, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "data", intValue: nil), _JSONKey(stringValue: "Index 0", intValue: 0), CodingKeys(stringValue: "attributes", intValue: nil), _JSONKey(stringValue: "poster_path", intValue: nil)], debugDescription: "Expected String but found null value instead.", underlyingError: nil)): file /Users/[User]/Developer/[App]/LatestVideosQuery.swift, line 35
2020-07-14 16:13:08.083721+0100 [App][57157:6135473] Fatal error: 'try!' expression unexpectedly raised an error: Swift.DecodingError.valueNotFound(Swift.String, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "data", intValue: nil), _JSONKey(stringValue: "Index 0", intValue: 0), CodingKeys(stringValue: "attributes", intValue: nil), _JSONKey(stringValue: "poster_path", intValue: nil)], debugDescription: "Expected String but found null value instead.", underlyingError: nil)): file /Users/[User]/Developer/[App]/LatestVideosQuery.swift, line 35
I get Swift is very strongly typed, but I’m unsure on how to encode this unstructured data in Swift. My plan is to have generic JSONAPIResource representations of resources coming back from my API, then I can then map into model structs in my Swift application, i.e. convert the above JSON:API resources into a Video struct.
Also, I’m trying to naïvely convert values in the attributes object to strings but as you may see, duration is an integer value. If there’s a way to have attributes in my JSONAPIResource struct retain primitive values such as integers and booleans and not just strings, I’d be keen to read how!
If it's a completely generic bag of key/values (which might indicate a need for a possible design change), you can create an enum to hold the different (primitive) values that JSON can hold:
enum JSONValue: Decodable {
case number(Double)
case integer(Int)
case string(String)
case bool(Bool)
case null
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let int = try? container.decode(Int.self) {
self = .integer(int)
} else if let double = try? container.decode(Double.self) {
self = .number(double)
} else if let string = try? container.decode(String.self) {
self = .string(string)
} else if let bool = try? container.decode(Bool.self) {
self = .bool(bool)
} else if container.decodeNil() {
self = .null
} else {
// throw some DecodingError
}
}
}
and then you could set attributes to:
var attributes: [String: JSONValue]
If there is a consistent relationship between the type value and the key-value pairs in attributes I recommend to declare attributes as enum with associated types and decode the type depending on the type value.
For example
let jsonString = """
{
"data": {
"type": "video",
"id": "1",
"attributes": {
"name": "Test Video",
"duration": 1234
}
}
}
"""
enum ResourceType : String, Decodable {
case video
}
enum AttributeType {
case video(Video)
}
struct Video : Decodable {
let name : String
let duration : Int
}
struct Root : Decodable {
let data : Resource
}
struct Resource : Decodable {
let type : ResourceType
let id : String
let attributes : AttributeType
private enum CodingKeys : String, CodingKey { case type, id, attributes }
init(from decoder : Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.type = try container.decode(ResourceType.self, forKey: .type)
self.id = try container.decode(String.self, forKey: .id)
switch self.type {
case .video:
let videoAttributes = try container.decode(Video.self, forKey: .attributes)
attributes = .video(videoAttributes)
}
}
}
let data = Data(jsonString.utf8)
do {
let result = try JSONDecoder().decode(Root.self, from: data)
print(result)
} catch {
print(error)
}

How do I support a variable JSON field of array(array,string) in Swift?

all.
I'm trying to create a struct to hold the contents of an API call that has a certain structure, but not sure how to write it.
Here's one result from the API where I'm having issues coding the receiving struct for:
"10E": {
"baseSetSize": 264,
"block": "Guilds of Ravnica",
"boosterV3": [
[
"rare",
"mythic rare"
],
"uncommon",
"uncommon",
"uncommon",
"common",
"common",
"common",
"common",
"common",
"common",
"common",
"common",
"common",
"common",
"land",
"marketing"
],
"cards": [ ... ]
}
The API lists the 'boosterV3' key as having the format array(array, string). However, some the returned data contains an array of 16 strings, and at least one has an array of strings as it's first element.
I've tried using the following struct:
struct MtGSet : Codable {
let name: String
let baseSetSize: Int
let block: String
let boosterV3: [String]
let cards: [MtGCard]
}
But running against the API with this generates this error:
[CodingKeys(stringValue: "boosterV3", intValue: nil), _JSONKey(stringValue: "Index 0", intValue: 0)],
debugDescription: "Expected to decode String but found an array instead.", underlyingError: nil))
Is it possible to write a data structure in Swift that would accept either 16 String values or 15 String values and 1 array of String for the boosterV3 key?
Thanks.
Try like the below. I am not testing this I just created by converting JSON to Swift. you can also do it by yourself, here it is https://app.quicktype.io/.
struct MtGSet : Codable {
let name: String
let baseSetSize: Int
let block: String
let boosterV3: [BoosterV3] // user [] of BoosterV3 instead of string
let cards: [MtGCard]
}
enum BoosterV3: Codable {
case string(String)
case stringArray([String])
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let x = try? container.decode([String].self) {
self = .stringArray(x)
return
}
if let x = try? container.decode(String.self) {
self = .string(x)
return
}
throw DecodingError.typeMismatch(BoosterV3.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for BoosterV3"))
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .string(let x):
try container.encode(x)
case .stringArray(let x):
try container.encode(x)
}
}
}

Swift json decode with dynamic keys

i'm trying to decode a nested json with dynamic keys, but can't find a solution.
here's my json:
{
"available_channels": {
"1000": {
"creation_date": "1111222",
"category_id": "9"
},
"1001": {
"creation_date": "222333",
"category_id": "10"
}
}
as you can see, "1000" and "1001" are dynamique.
The models i'm using:
struct StreamsData: Codable{
let availableChannels: AvailableChannels
}
struct AvailableChannels: Codable{
let channels: [String: Channel]
}
struct Channel: Codable{
let creationDate: String
let categoryId: String
}
'StreamsData' is the root object
'AvailableChannels' is the objects containing all channels objects
'Channel' channel model
decoding the json:
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let streams = try decoder.decode(StreamsData.self, from: data)
With this code i have this error:
CodingKeys(stringValue: "availableChannels", intValue: nil)
- debugDescription : "No value associated with key CodingKeys(stringValue: \"channels\", intValue: nil) (\"channels\")."
The problem is clear, as 'AvailableChannels' is declared to have a 'channel' property, the decoder is trying to find "channels" as key for the object containing the "creation_date".
Could you help me to solve this problem, thanks.
You only need
struct StreamsData: Codable{
let availableChannels: [String: Channel]
}
struct Channel: Codable{
let creationDate,categoryId: String
}
do {
let dec = JSONDecoder()
dec.keyDecodingStrategy = .convertFromSnakeCase
let res = try dec.decode(StreamsData.self, from: data)
}
catch {
print(error)
}

Swift 4 Decodable - No value associated with key CodingKeys [duplicate]

This question already has answers here:
How do I use custom keys with Swift 4's Decodable protocol?
(4 answers)
Closed 3 years ago.
I'm decoding a JSON response in my Swift App, and the code used to work till it decided to stop working.
this is my json reposnse
{
"foods": [
{
"food_name": "Milk Chocolate",
"brand_name": "Snickers",
"serving_weight_grams": 41.7,
"nf_calories": 212.3,
"nf_total_fat": 11.6,
"nf_saturated_fat": 4,
"nf_total_carbohydrate": 22.7,
"nf_protein": 3.9
}
]
}
And this is the code to decode my json
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
guard let data = data else { return }
print(String(data: data, encoding: .utf8)!)
do {
//Decode dataResponse received from a network request
let decoder = JSONDecoder()
let foods = try decoder.decode(JSONFoods.self, from: data) //Decode JSON Response Data
self.jsonfood = foods.JSONFood[0]
print(self.jsonfood!)
} catch let parsingError {
print("Error", parsingError)
}
}
task.resume()
And my Structs are
struct JSONFoods: Decodable {
var JSONFood: [JSONFood]
}
struct JSONFood: Decodable{
var food_name: String
var brand_name: String
var nf_calories: Int
var nf_protein: Int
var nf_total_fat: Int
var nf_total_carbohydrate: Int
var serving_weight_grams: Int
}
And the error message I get is this
keyNotFound(CodingKeys(stringValue: "JSONFood", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: "JSONFood", intValue: nil) ("JSONFood").", underlyingError: nil))
And if i get replace decode(JSONFoods.self, from: data) with decode(JSONFood.self, from: data)
I get this error message
keyNotFound(CodingKeys(stringValue: "food_name", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: "food_name", intValue: nil) ("food_name").", underlyingError: nil))
I searched everywhere with no luck, any help is very appreciated
You need
struct Root: Codable {
let foods: [Food]
}
struct Food: Codable {
let foodName: String?
let brandName: String
let servingWeightGrams, nfCalories, nfTotalFat: Double
let nfSaturatedFat: Int
let nfTotalCarbohydrate, nfProtein: Double
enum CodingKeys: String, CodingKey {
case foodName = "food_name"
case brandName = "brand_name"
case servingWeightGrams = "serving_weight_grams"
case nfCalories = "nf_calories"
case nfTotalFat = "nf_total_fat"
case nfSaturatedFat = "nf_saturated_fat"
case nfTotalCarbohydrate = "nf_total_carbohydrate"
case nfProtein = "nf_protein"
}
}
First : you make JSONFood while it should be foods
Second :food_name doesn't exist in current json root so this will fail
let foods = try decoder.decode(JSONFoods.self, from: data) //Decode JSON Response Data
In case to take advantage of convertFromSnakeCase
let str = """
{"foods":[{"food_name":"Milk Chocolate","brand_name":"Snickers","serving_weight_grams":41.7,"nf_calories":212.3,"nf_total_fat":11.6,"nf_saturated_fat":4,"nf_total_carbohydrate":22.7,"nf_protein":3.9}]}
"""
do {
let res = JSONDecoder()
res.keyDecodingStrategy = .convertFromSnakeCase
let ss = try res.decode(Root.self, from:Data(str.utf8))
print(ss)
}
catch {
print(error)
}
struct Root: Codable {
let foods: [Food]
}
struct Food: Codable {
let foodName: String?
let brandName: String
let servingWeightGrams, nfCalories, nfTotalFat: Double
let nfSaturatedFat: Int
let nfTotalCarbohydrate, nfProtein: Double
}