If I have ArticleItem struct like this:
public struct ArticleItem: Equatable {
public let id: UUID
public let description: String?
public let location: String?
public let thumbnailURL: URL
public let created: Date
public init(id: UUID, description: String?, location: String?, thumbnailURL: URL, created: Date) {
self.id = id
self.description = description
self.location = location
self.thumbnailURL = thumbnailURL
self.created = created
}
}
But from back-end we would receive such info:
{
"id": "a UUID",
"description": "a description",
"location": "a location",
"image": "https://a-image.url",
"created_date": "2020-05-09",
}
How can I test the correctness of Date ?
In the test I have something like this:
func test_load_deliversItemsOn200HTTPResponseWithJSONItems(){
let (sut, client) = makeSUT()
let item1 = ArticleItem(id: UUID(), description: nil, location: nil, thumbnailURL: URL(string: "http://a-url.com/")!, created: Date())
let item1JSON = [
"id": item1.id.uuidString,
"image": item1.imageURL.absoluteString,
"created_date": ??? // what should be here
]
let items = [
"items": [item1JSON]
]
expect(sut: sut, toCompleteWithResult: .success([item1])) {
let itemsJSON = try! JSONSerialization.data(withJSONObject: items)
client.complete(withStatusCode: 200, data: itemsJSON)
}
}
I've heard about using iso8601 when decoding but still don't know how to do it.
Please help me. Thanks
You need to tell the decoder what dateDecoderStrategy your model or json follows.
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(TheFormatThatTheDateHas)
and then
decoder.decode........
for the format, google the date plus with format.
Related
I'm using Alamofire to decode objects, and my JSON looks like this
{
"wallet": {
"public_key": "ADDRESS",
"name": "My test wallet",
"id": "-MQ9NdAyMaK3WQfSOYZW",
"owner": "Kca8BNHy8FemIxPO7FWBSLE8XKN2",
"currency": "ME",
"metadata": {
"legacy_address": "WHAT",
"x_pub_address": "SOMETHING COOL"
}
},
"secrets": {
"wif": "SOMETHING",
"private_key": "SOMETHING ELSE",
"mnemonic": "YOU WISH"
}
}
My Swift looks like this:
class Response: Codable {
var wallet: Wallet
var secrets: [String: String]
}
class Wallet: Codable {
var publicKey: String
var name: String
var id: String
var owner: String
var currency: String
var metadata: [String: String]
enum WalletKeys: String, CodingKey{
case publicKey = "public_key",
case name,
case id
case owner,
case currency,
case metadata
}
}
I'm getting a keyNotFound(CodingKeys(stringValue: "publicKey")) error, and I dont know why. Can someone help me figure this out?
You need to set your decoder keyDecodingStrategy property value to .convertFromSnakeCase:
struct Response: Codable {
let wallet: Wallet
let secrets: Secrets
}
struct Secrets: Codable {
let wif: String
let privateKey: String
let mnemonic: String
}
struct Wallet: Codable {
let publicKey: String
let name: String
let id: String
let owner: String
let currency: String
let metadata: Metadata
}
struct Metadata: Codable {
let legacyAddress: String
let xPubAddress: String
}
let json = """
{
"wallet": {
"public_key": "ADDRESS",
"name": "My test wallet",
"id": "-MQ9NdAyMaK3WQfSOYZW",
"owner": "Kca8BNHy8FemIxPO7FWBSLE8XKN2",
"currency": "ME",
"metadata": {
"legacy_address": "WHAT",
"x_pub_address": "SOMETHING COOL"
}
},
"secrets": {
"wif": "SOMETHING",
"private_key": "SOMETHING ELSE",
"mnemonic": "YOU WISH"
}
}
"""
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let root = try decoder.decode(Response.self, from: Data(json.utf8))
print(root)
} catch {
print(error)
}
This will print:
Response(wallet: Wallet(publicKey: "ADDRESS", name: "My test wallet", id: "-MQ9NdAyMaK3WQfSOYZW", owner: "Kca8BNHy8FemIxPO7FWBSLE8XKN2", currency: "ME", metadata: Metadata(legacyAddress: "WHAT", xPubAddress: "SOMETHING COOL")), secrets: __lldb_expr_1.Secrets(wif: "SOMETHING", privateKey: "SOMETHING ELSE", mnemonic: "YOU WISH"))
I have swiftyJSON added as a package dependancy and have the appropriate import statement.
My JSON file has the following format:
"00AK": {
"icao": "00AK",
"iata": "",
"name": "Lowell Field",
"city": "Anchor Point",
"state": "Alaska",
"country": "US",
"elevation": 450,
"lat": 59.94919968,
"lon": -151.695999146,
"tz": "America\/Anchorage"
},
"00AL": {
"icao": "00AL",
"iata": "",
"name": "Epps Airpark",
"city": "Harvest",
"state": "Alabama",
"country": "US",
"elevation": 820,
"lat": 34.8647994995,
"lon": -86.7703018188,
"tz": "America\/Chicago"
etc. And the JSON file is stored within the app bundle
This is the code I have currently:
import SwiftUI
import SwiftyJSON
let path = Bundle.main.url(forResource: "airports", withExtension: "json")
let jsonString = try? String(contentsOf: path!)
let dataFromString = jsonString!.data
let json = JSON(data: dataFromString)
This gives me the error on the last line of....
"Cannot convert value of type ' (String.Encoding, Bool) -> Data?' to expected argument type 'Data'"
If I 'mouseover' the jsonString var the tooltip shows me the correct string from the file, but I don't understand why the next bit just isn't working.
Your problem seems to be related to convert to Data, this is how I would read the file and convert to Data
let url = Bundle.main.url(forResource: "airports", withExtension: "json")!
let jsonString = try! String(contentsOf: url)
let dataFromString = jsonString.data(using: .utf8)!
To move on here is a simplified example how you can use Codable for your json
struct Airport: Codable {
let icao: String
let name: String
let city: String
let country: String
let elevation: Int
let latitude: Double
let longitude: Double
enum CodingKeys: String, CodingKey {
case latitude = "lat"
case longitude = "lon"
case icao, name, city, country, elevation
}
}
do {
let result = try JSONDecoder().decode([String: Airport].self, from: dataFromString)
let airports = result.values
print(airports)
} catch {
print(error)
}
Note that I did not include all fields here and also made some assumptions what the file content looked like since you have only included a part of it.
I have a model that looks like:
public struct Profile {
public let bio: String?
public let company: String
public let createdDate: Date
public let department: String
public let email: String
public let firstName: String
public let coverImageURL: URL?
public let jobTitle: String
public let lastName: String
public let location: String?
public let name: String
public let profileImageURL: URL?
public let roles: [String]
public let isActive: Bool
public let updatedDate: Date?
public let userID: String
public init(
bio: String?, company: String, createdDate: Date, department: String, email: String, firstName: String, coverImageURL: URL?, jobTitle: String,
lastName: String, location: String?, name: String, profileImageURL: URL?, roles: [String], isActive: Bool, updatedDate: Date?, userID: String) {
self.bio = bio
self.company = company
self.createdDate = createdDate
self.department = department
self.email = email
self.firstName = firstName
self.coverImageURL = coverImageURL
self.jobTitle = jobTitle
self.lastName = lastName
self.location = location
self.name = name
self.profileImageURL = profileImageURL
self.roles = roles
self.isActive = isActive
self.updatedDate = updatedDate
self.userID = userID
}
}
extension Profile: Equatable { }
I am trying to create a tuple that represents it as (model: Profile, json: [String: Any])
using the following method -
func makeProfile() -> (model: Profile, json: [String: Any]) {
let updatedDate = Date()
let updatedDateStr = updatedDate.toString(dateFormat: "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX")
let createdDate = Date()
let createdDateStr = createdDate.toString(dateFormat: "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX")
let coverImageURL = makeURL("https://headerUri.com")
let profileImageURL = makeURL("https://pictureUri.com")
let model = Profile(
bio: "Some Bio",
company: "Cool Job INC",
createdDate: createdDate,
department: "Engineering",
email: "name#domain.tld",
firstName: "Anne",
coverImageURL: coverImageURL,
jobTitle: "Test Dummy",
lastName: "Employee",
location: "London",
name: "Anne Employee",
profileImageURL: profileImageURL,
roles: ["ADMIN"],
isActive: true,
updatedDate: updatedDate,
userID: UUID().uuidString
)
let json: [String: Any] = [
"bio": model.bio,
"company": model.company,
"createdDate": createdDateStr,
"department": model.department,
"email": model.email,
"firstName": model.firstName,
"headerUri": model.coverImageURL?.absoluteString,
"jobTitle": model.jobTitle,
"lastName": model.lastName,
"location": model.location,
"name": model.name,
"pictureUri": model.profileImageURL?.absoluteString,
"roles": model.roles,
"isActive": model.isActive,
"updatedDate": updatedDateStr,
"userId": model.userID
].compactMapValues { $0 }
return (model: model, json: json)
}
}
extension Date {
func toString(dateFormat format: String) -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = format
return dateFormatter.string(from: self)
}
}
All of the optional properties are showing a warning Expression implicitly coerced from 'String?' to 'Any'
I have added .compactMapValues { $0 } to the dict but has had no effect.
How can I clear this warning?
You're getting an error because you trying to implicitly coerce an optional value (which could be nil) to Any, which hides that the value could be nil.
compactMapValues doesn't guarantee at compile-time that the value is not nil; all the compiler knows is that something that is an optional (like String?) is now treated as a non-optional Any.
One way you could avoid the error is by providing a default value instead of the optional:
let json: [String: Any] = [
"bio": model.bio ?? ""
// ...
]
Alternatively, you can create a dictionary of: [String, Any?] to assign values to, and then use .compactMapValues, which would give you back non-optional values of Any.
let withOptionals: [String: Any?] = [
"bio": model.bio,
// ...
]
let json: [String, Any] = withOptionals.compactMapValues { $0 }
You're getting the warning because the values are optional when you initialize the dictionary. Your .compactMapValues { $0 } won't help because by then it's "too late".
Instead you could initialize an empty dictionary and add the values 1 by 1.
var json = [String: Any]()
json["bio"] = model.bio
json["company"] = model.company
// all of the other values
json["userId"] = model.userID
I'm saving this API response in Core Data successfully:
{
"data": [{
"result": {
"id": 5,
"key": "A",
"title": "some title",
"comments": "some comments",
"created_at": "2019-12-16 13:42:26",
"updated_at": "2019-12-16 13:42:26",
"deleted_at": ""
}
}, {
"result": {
"id": 7,
"key": "B",
"title": "some title",
"comments": "some comments",
"created_at": "2019-12-16 13:45:29",
"updated_at": "2019-12-16 13:45:29",
"deleted_at": ""
}
}]
}
And I'm able to retrieve it from Core Data, but I need to retrieve only the array that has "key": "A"
So I'm trying to do it this way but getting nothing returned:
func getResultA() -> [ResultsModel] {
var retrievedResults = [ResultsModel]()
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Results")
fetchRequest.predicate = NSPredicate(format: "key == %#", "A")
let result = try? privateContext.fetch(fetchRequest)
for data in result as! [NSManagedObject] {
let id = data.value(forKey: "id") as? Int ?? 0
let key = data.value(forKey: "key") as? String ?? ""
let title = data.value(forKey: "title") as? String ?? ""
let comments = data.value(forKey: "comments") as? String ?? ""
retrievedResults.append(ResultsModel(id: id, key: key, title: title, comments: comments))
}
return retrievedResults
}
Here is the ResultsModel if it helps:
class ResultsModel {
let id: Int
let key: String
let title: String
let comments: String
init(id: Int, key: String, title: String, comments: String) {
self.id = id
self.key = key
self.title = title
self.comments = comments
}
}
I think I'm having a mistake using NSPredicate I need to filter only the array that have Key = A or B .
Can anyone point out the mistake please? Thank you in advance for your ideas and time!
I am new to iOS, and want to parse the JSON using Decodable but cant get through this, how should I work this out?
The view controller where I am trying to parse the data
class ViewController: UIViewController {
var servers = [Server]()
let apiUrl = "https://someurl/api/dashboard"
override func viewDidLoad() {
super.viewDidLoad()
guard let url = URL(string: self.apiUrl) else { return }
getDataFrom(url)
}
fileprivate func getDataFrom(_ url: URL) {
URLSession.shared.dataTask(with: url){ (data, response, error) in
guard let data = data else { return }
do {
let apiResponse = try JSONDecoder().decode(Server.self, from: data)
print(apiResponse)
} catch let jsonError {
print(jsonError)
}
}.resume()
}
}
The Server.swift file where I am confirming to the decodable protocol
struct Server: Decodable {
let current_page: Int
let data: [ServerData]
let first_page_url: String
}
struct ServerData: Decodable {
let hostname: String
let ipaddress: String
let customer: [Customer]
let latest_value: [LatestValue]
}
struct Customer: Decodable {
let name: String
let contact_person :String
let email: String
}
struct LatestValue: Decodable {
let systemuptime: String
let memtotal: Float
let memfree: Double
let loadaverage: Float
}
No value associated with key CodingKeys I get this error,
The response from the server
{
"servers": {
"current_page": 1,
"data": [
{
"hostname": "johndoes",
"ipaddress": "10.0.2.99",
"id": 7,
"latest_value_id": 1130238,
"customers": [
{
"name": "Jane Doe",
"contact_person": "John Doe",
"id": 2,
"email": "john.#example.com",
"pivot": {
"server_id": 7,
"customer_id": 2
}
}
],
"latest_value": {
"id": 1130238,
"server_id": 7,
"systemuptime": "80days:10hours:23minutes",
"memtotal": 3.7,
"memfree": 1.6400000000000001,
"loadaverage": 2.25,
"disktotal": {
"dev-mapper-centos-root_disktotal": "38",
"dev-mapper-proquote-xfs-lvm_disktotal": "200"
},
"diskused": "{\"dev-mapper-centos-root_diskused\":\"16\",\"dev-mapper-proquote-xfs-lvm_diskused\":\"188\"}",
"custom_field": "[]",
"additional_attributes": {
"fathom": {
"name": "fathom",
"status": 1
},
"trenddb": {
"name": "trenddb",
"status": 1
},
"trendwi": {
"name": "trendwi",
"status": 1
},
"appsrv": {
"name": "appsrv",
"status": 1
}
},
"created_at": "2019-06-15 02:25:02",
"updated_at": "2019-06-15 02:25:02"
}
}
]
},
"message": "Success"
}
You seem to have few different errors in your data structure.
First of all, you are trying to decode Server while your json has servers inside a dict {"servers": ... }, So use a parent root object for it.
Your latest_value inside ServerData is defined as array, while it should be LatestValue struct not [LatestValue].
There is no first_page_url element in your json, but your Server struct has the property, make it optional, so that JSONDecoder decodes it only if it is present.
Here is your refined data models.
struct Response: Decodable {
let servers: Server
}
struct Server: Decodable {
let current_page: Int
let data: [ServerData]
let first_page_url: String?
}
struct ServerData: Decodable {
let hostname: String
let ipaddress: String
let customers: [Customer]
let latest_value: LatestValue
}
struct Customer: Decodable {
let name: String
let contact_person :String
let email: String
}
struct LatestValue: Decodable {
let systemuptime: String
let memtotal: Float
let memfree: Double
let loadaverage: Float
}
And decode Response instead of decoding Server, like so,
do {
let apiResponse = try JSONDecoder().decode(Response.self, from: data)
let server = apiResponse.server // Here is your server struct.
print(server)
} catch let jsonError {
print(jsonError)
}