Issue
I have a problem about decodable the "product" from laravel api request. Error is "The data couldn´t be read because it isn´t in the correct format.
request with insomnia is right and i get the product where the ean number has.
Laravel for database:
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->bigInteger('ean');
$table->string('name');
$table->string('manufacturer');
$table->string('productNumber');
$table->longText('description')->nullable();
$table->string('propertie')->nullable();
$table->string('storagePlace');
$table->integer('inStock')->nullable();
$table->integer('unit');
$table->longText('note')->nullable();
$table->string('department')->default("zvk");
$table->timestamps();
Swift Product decodable:
class Product: Decodable {
var id: Int
var ean: Double
var name: String
var manufacturer: String
var productNumber: String
var description: String
var propertie: String
var storagePlace: String
var inStock: Int
var unit: Int
var note: String
var department: String
var created_at: Date
var updated_at: Date
enum CodingKeys: String, CodingKey {
case id
case ean
case name
case manufacturer
case productNumber
case description
case propertie
case storagePlace
case inStock
case unit
case note
case department
case created_at
case updated_at
}
public required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int.self, forKey: .id)
self.ean = try container.decode(Double.self, forKey: .ean)
self.name = try container.decode(String.self, forKey: .name)
self.manufacturer = try container.decode(String.self, forKey: .manufacturer)
self.productNumber = try container.decode(String.self, forKey: .productNumber)
self.description = try container.decode(String.self, forKey: .description)
self.propertie = try container.decode(String.self, forKey: .propertie)
self.storagePlace = try container.decode(String.self, forKey: .storagePlace)
self.inStock = try container.decode(Int.self, forKey: .inStock)
self.unit = try container.decode(Int.self, forKey: .unit)
self.note = try container.decode(String.self, forKey: .note)
self.department = try container.decode(String.self, forKey: .department)
self.created_at = try container.decode(Date.self, forKey: .created_at)
self.updated_at = try container.decode(Date.self, forKey: .updated_at)
}
}
Swift short code:
AF.request(productWithEanUrlRequest, method: .get, parameters: parameters, headers: headers).responseJSON { response in
ProgressHUD.dismiss()
//Gets HTTP status code
let statusCode = (response.response?.statusCode)
switch statusCode {
case 200:
//OK
do {
let product = try JSONDecoder().decode([Product].self, from: response.data!)
debugPrint(product)
} catch let error as NSError {
AlertView.showAlertView(with: "Error", and: error.localizedDescription, in: self)
}
Preview from request:
{
"product": [
{
"id": 1,
"ean": "1234567890",
"name": "Name",
"manufacturer": "Hersteller",
"productNumber": "Artikel-Nr.",
"description": "Beschreibung vom Artikel",
"propertie": "Eigenschaften",
"storagePlace": "Lagerplatz",
"inStock": "12",
"unit": "0",
"note": "Irgendwelche Notiz",
"department": "zvs",
"created_at": "2022-01-12T10:20:51.000000Z",
"updated_at": "2022-01-12T10:20:51.000000Z"
}
]
i can´t see the error i made!?
Solution
Don't use error.localizedError
, use error
, so print it, it will give more useful informations. You'll see it along the answers, which I made step by step.
You are missing the fact that your JSON is a top level a Dictionary, it's {"product": [Product1, Product2]}
, if its was [Product1, Product2]
, your decoding would work, but you need to parsed first the key "product"
.
There is no need for enum CodingKeys: String, CodingKey
if your variables names match the key name from JSON, and no need for init(from decoder: Decoder)
if you self.variable = container.decode(VariableType.self, forKey: .variable)
So let's replicate your issue:
let jsonStr = """
{
"product": [
{
"id": 1,
"ean": "1234567890",
"name": "Name",
"manufacturer": "Hersteller",
"productNumber": "Artikel-Nr.",
"description": "Beschreibung vom Artikel",
"propertie": "Eigenschaften",
"storagePlace": "Lagerplatz",
"inStock": "12",
"unit": "0",
"note": "Irgendwelche Notiz",
"department": "zvs",
"created_at": "2022-01-12T10:20:51.000000Z",
"updated_at": "2022-01-12T10:20:51.000000Z"
}
]
}
"""
class Product: Decodable {
var id: Int
var ean: Double
var name: String
var manufacturer: String
var productNumber: String
var description: String
var propertie: String
var storagePlace: String
var inStock: Int
var unit: Int
var note: String
var department: String
var created_at: Date
var updated_at: Date
}
do {
let products = try JSONDecoder().decode([Product].self, from: Data(jsonStr.utf8))
print(products)
} catch {
print("Error while decoding: \(error)")
}
Now, printing error
instead of error.localizedDescription
gives better input:
$>Error while decoding: typeMismatch(Swift.Array<Any>, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Array<Any> but found a dictionary instead.", underlyingError: nil))
As said, you are missing the top level. If you don't see it, let's do the reverse:
Let's start by making Product
Codable
(ie, Encodable
too):
class Product: Decodable {
=> class Product: Codable {
, and now class Product: Codable {
=> struct Product: Codable {
, because it will get a automatic init(id:ean:etc...)
.
Then, you wrote [Product].self
, so you expect your JSON to be of that type, an array of Product
.
Let's see what it should look likes then:
do {
let products: [Product] = [Product(id: 3, ean: 3.4, name: "Product Name", manufacturer: "Manufacturer Name", productNumber: "Product Number", description: "Product description", propertie: "Product Properties", storagePlace: "Product Storage Place", inStock: 4, unit: 5, note: "Product note", department: "Product Department", created_at: Date(), updated_at: Date())]
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted //Because it's more human readable
let encodedProductsJSONAsData = try encoder.encode(products)
let encodedProductsJSONAsString = String(data: encodedProductsJSONAsData, encoding: .utf8)!
print(encodedProductsJSONAsString)
} catch {
print("Error while encoding: \(error)")
}
Output:
[
{
"id" : 3,
"unit" : 5,
"description" : "Product description",
"productNumber" : "Product Number",
"note" : "Product note",
"created_at" : 664108170.25946903,
"propertie" : "Product Properties",
"ean" : 3.3999999999999999,
"storagePlace" : "Product Storage Place",
"manufacturer" : "Manufacturer Name",
"updated_at" : 664108170.25946999,
"inStock" : 4,
"department" : "Product Department",
"name" : "Product Name"
}
]
So that's what the [Products] -> JSON looks like. Does it match your JSON?
No, you see it's no the same, there is a "product"
level.
So let's add:
struct ProductsAPIResponse: Codable {
let product: [Product]
}
and change the decoding to:
do {
let productsAPIResponse = try JSONDecoder().decode(ProductsAPIResponse.self, from: Data(jsonStr.utf8))
print(productsAPIResponse)
let products = productsAPIResponse.product
print(products)
} catch {
print("Error while decoding: \(error)")
}
Now, the output is:
$>Error while decoding: typeMismatch(Swift.Double, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "product", intValue: nil), _JSONKey(stringValue: "Index 0", intValue: 0), CodingKeys(stringValue: "ean", intValue: nil)], debugDescription: "Expected to decode Double but found a string/data instead.", underlyingError: nil))
Ok, ean
is in fact a String
, not an Int
, so change either the Product
model, or fix it in your server side.
You'll get the same kind of error for inStock
& unit
. Same solution.
Now, you'll get:
$>Error while decoding: typeMismatch(Swift.Double, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "product", intValue: nil), _JSONKey(stringValue: "Index 0", intValue: 0), CodingKeys(stringValue: "created_at", intValue: nil)], debugDescription: "Expected to decode Double but found a string/data instead.", underlyingError: nil))
By default, the date decoding expects a unixtimestamp, so it awaits for a Double
. Here, you have a String
representing the date, so you need a DateFormatter
.
Let's fix that:
let decoder = JSONDecoder()
let dateFormatter = ISO8601DateFormatter()
dateFormatter.formatOptions.insert(.withFractionalSeconds)
decoder.dateDecodingStrategy = .custom({ decoder in
let container = try decoder.singleValueContainer()
let dateStr = try container.decode(String.self)
return dateFormatter.date(from: dateStr) ?? Date()
})
let productsAPIResponse = try decoder.decode(ProductsAPIResponse.self, from: Data(jsonStr.utf8))
Not, it could have just been decoder.dateDecodingStrategy = .iso8601
if your date strings looks like "2022-01-17T10:44:50Z"
, but there is need for the fractional seconds to decode...
Now, it should works.
Answered By - Larme
0 Comments:
Post a Comment
Note: Only a member of this blog may post a comment.