first of all, sorry for the bad english.
I'm having a little issue with the app for my company, I started learning Kotlin a few months ago, so it's everything pretty new to me, i did a little digging for most of my problems but this one I didn't find anywhere.
We have a server provind data with a Joomla API, the problem is, when I use retrofit2 to get the data with a query, it is possible to return a BEGIN_OBJECT when no data is found, and a BEGIN_ARRAY when the data is found. I found a lot of places telling when it's one but is expected another, but the two in the same response, not yet.
This is the response when the data is not found:
{"err_msg":"Produto n\u00e3o encontrado (CALOTA CORSA)","err_code":404,"response_id":"","api":"","version":"1.0","data":{}}
I called the data class for this piece ProductList, and the data I called Product, for future reference...
This is the response when data is found:
{"err_msg":"","err_code":"","response_id":522,"api":"app.produto","version":"1.0","data":[{"codigo":"0340008","filial":"CPS","referencia":"7898314110118","ncm":"38249941","codigosecundario":"146","nome":"WHITE LUB SUPER AEROSSOL 300ML 146","similar":"0012861","parceiro":"","produtosrelacionados":"0012861;0125121;0125945;0340008;0340035;0340169;0343394;0582033;0582954;0610250;1203682;1227480;1227569;1366196;1366761;1450241;1450861","marca":"ORBI QUIMICA","linha":"DESENGRIPANTE","lancamento":"2011-07-28 00:00:00","quantidadeembalagem":"12","unidademedida":"PC","caracteristicas":"OLEO WHITE LUB SUPER AEROSSOL 300ML - DESENGRIPANTE\/ LUBRIFICANTE\/ PROTETIVO 146","lado":"N\/A","ultima_atualizacao_preco":"2022-08-05 10:32:53","valor":"9.99","ultima_atualizacao_estoque":"2022-09-01 00:03:17","estoque":"200"}]}
When the response is successful, it is possible to recieve up to 10 different products.
This is my retrofit call at the ViewModel, I'm using GsonConverterFactory with retrofit.
fun getProductByCode(code: String, token: String) {
RetrofitInstance.chgApi.listProducts(code, token).enqueue(object : Callback<ProductList> {
override fun onResponse(call: Call<ProductList>, response: Response<ProductList>) {
if (response.body()?.errCode != "") {
Log.e("Response", response.body()?.errMsg!!)
errMsg.value = response.body()?.errMsg!!
} else {
errMsg.value = ""
products.value = response.body()!!.data
}
}
override fun onFailure(call: Call<ProductList>, t: Throwable) {
Log.e("Error", t.message.toString())
}
})
}
First data class
data class ProductList(
#SerializedName("err_msg") var errMsg : String,
#SerializedName("err_code") var errCode : String,
#SerializedName("response_id") var responseId : String,
#SerializedName("api") var api : String,
#SerializedName("version") var version : String,
#SerializedName("data") var data: ArrayList<Product>
)
Second data class
#Entity(tableName = PRODUCT_DATABASE)
data class Product(
#PrimaryKey
#SerializedName("codigo" ) var codigo : String,
#SerializedName("filial" ) var filial : String,
#SerializedName("referencia" ) var referencia : String,
#SerializedName("ncm" ) var ncm : String,
#SerializedName("codigosecundario" ) var codigosecundario : String,
#SerializedName("nome" ) var nome : String,
#SerializedName("similar" ) var similar : String,
#SerializedName("parceiro" ) var parceiro : String,
#SerializedName("produtosrelacionados" ) var produtosrelacionados : String,
#SerializedName("marca" ) var marca : String,
#SerializedName("linha" ) var linha : String,
#SerializedName("lancamento" ) var lancamento : String,
#SerializedName("quantidadeembalagem" ) var quantidadeembalagem : String,
#SerializedName("unidademedida" ) var unidademedida : String,
#SerializedName("caracteristicas" ) var caracteristicas : String,
#SerializedName("lado" ) var lado : String,
#SerializedName("ultima_atualizacao_preco" ) var ultimaAtualizacaoPreco : String,
#SerializedName("valor" ) var valor : String,
#SerializedName("ultima_atualizacao_estoque" ) var ultimaAtualizacaoEstoque : String,
#SerializedName("estoque" ) var estoque : String,
var cesta : Int,
var comprar : Boolean
)
The simple way to treat would be to change my data class, changing the type of the field "data" to ArrayList< Product > or to only Product, but, as far as I know, it can't be both at the same time... Any suggestions?
Assuming that, as shown in your answer, you have two separate model classes, one for a successful response and one for an error response, and a common supertype (for example an interface Response), you could solve this with a custom JsonDeserializer 1. It should based on the members and the values of the JsonObject decide as which type the data should be deserialized. This way you can keep data: List<Product> for the ProductList response.
object ProductListResponseDeserializer : JsonDeserializer<Response> {
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Response {
val jsonObject = json.asJsonObject
val errCode = jsonObject.getAsJsonPrimitive("err_code").asString
val errMsg = jsonObject.getAsJsonPrimitive("err_msg").asString
val responseType = if (errCode.isEmpty() && errMsg.isEmpty())
ProductList::class.java
else ProductListError::class.java
return context.deserialize(json, responseType)
}
}
(Note: Instead of duplicating the strings "err_code" and "err_msg" here and in your model classes, you could also use a single constant which is read here and used for the #SerializedName in your model classes.)
You would then have to create a GsonBuilder, register the deserializer and use Retrofit's GsonConverterFactory to use the custom Gson instance:
val gson = GsonBuilder()
.registerTypeAdapter(Response::class.java, ProductListResponseDeserializer)
.create()
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(...)
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
And in your callback check the class of the Response instance (whether it is a ProductList or a ProductListError).
1: In general TypeAdapter should be preferred over JsonDeserializer because it is more performant, but because here the data needs to be parsed as JsonObject anyway, there is most likely no difference.
Long story short, it took me the whole day and I found a solution here:
how to handle two different Retrofit response in Kotlin?
Just changing my Callback, Call and Response to < Any >, creating a new model and doing some treatment acording to the response. The final code:
fun searchProduct(code: String, token: String) {
RetrofitInstance.chgApi.listProducts(code.uppercase(), token).enqueue(object : Callback<Any> {
override fun onResponse(call: Call<Any>, response: Response<Any>) {
val gson = Gson()
if (response.body().toString().contains("err_msg=, err_code=")) {
productList = gson.fromJson(gson.toJson(response.body()), ProductList::class.java)
products.value = productList.data
} else {
productListError = gson.fromJson(gson.toJson(response.body()), ProductListError::class.java)
errMsg.value = productListError.errMsg
}
}
override fun onFailure(call: Call<Any>, t: Throwable) {
Log.e("Error", t.message.toString())
}
})
}
I have a function to get JSON and put the value into a struct (Job). It prints out the value without optional for the var. But it prints out the struct var with optional. Please kindly help to solve this issue.
var newJob = Job()
var joblist:[Job] = []
func parseJSON(json:Any){
if let okJSON = json as? [Any]{
for item in okJSON {
let infoDictionary = item as! [String:String]
if let activityid = infoDictionary["ActivityID"]
{
newJob.ActivityID=activityid
print(activityid)
print(newJob.ActivityID)
}
if let companyname = infoDictionary["CompanyName"] {newJob.CompanyName=companyname}
if let quantity = infoDictionary["Quantity"] {newJob.Quantity=quantity}
if let coupontitle = infoDictionary["Title"] {newJob.CouponTitle=coupontitle}
if let couponterms = infoDictionary["Terms"] {newJob.CouponTerms=couponterms}
if let expirdate = infoDictionary["ExpirDate"] {newJob.ExpirDate=expirdate}
if let contactperson = infoDictionary["ContactPerson"] {newJob.ContactPerson=contactperson}
if let tel = infoDictionary["TEL"] {newJob.TEL=tel}
joblist.append(newJob)
}
print(joblist)
}
}
Here with the print result:
3
Optional("3")
2
Optional("2")
1
Optional("1")
[cateringhk.Job(ActivityID: Optional("3"), CompanyName: Optional("ABC\351\233集\351\233集\345\351\233集\345\345\234團"), Quantity: Optional("5"), CouponTitle: Optional("$30現金卷"), CouponTerms: Optional("消費滿$100可以使用\r\n台灯固定环E27灯头 \r\n黑色白色固定扣 \r\n台灯灯罩床头灯具固定环配件 \r\n[交易快照]"), ExpirDate: Optional("2017-11-24"), ContactPerson: Optional("陳先生"), TEL: Optional("96855000")), cateringhk.Job(ActivityID: Optional("2"), CompanyName: Optional("皇上皇點心集團"), Quantity: Optional("31"), CouponTitle: Optional("$30現金卷"), CouponTerms: Optional("消費滿$100可以使用"), ExpirDate: Optional("2017-11-24"), ContactPerson: Optional("陳先生"), TEL: Optional("96855000")), cateringhk.Job(ActivityID: Optional("1"), CompanyName: Optional("八樂園酒樓"), Quantity: Optional("22"), CouponTitle: Optional("$20消費券"), CouponTerms: Optional("每1帳單只可以使用一用一\345\274張消費券"), ExpirDate: Optional("2017-11-24"), ContactPerson: Optional("陳小姐"), TEL: Optional("94567821"))]
This behavior is normal if the properties in the struct are also declared as optionals. In this case the unwrapping with optional binding has actually no effect.
To avoid that declare the property as non-optional and assign a default value for example
struct Job {
var activityID = ""
...
}
newJob.activityID = infoDictionary["ActivityID"] ?? ""
But assigning an empty string twice looks cumbersome. I'd add an initializer to the struct to take a dictionary, declare the properties as constants and handle the default value in the init method.
struct Job {
let activityID : String
...
init(dict: [String:String]) {
activityID = dict["ActivityID"] ?? ""
...
}
}
Note:
Please conform to the naming convention that variable names start with a lowercase letter
the error "Type 'Any' has no subscript members." in line return at $0["deadline"].
this only happened when I converted syntax to swift 3.
TodoList file
fileprivate let ITEMS_KEY = "todoItems"
func allItems() -> [TodoItem] {
let todoDictionary = UserDefaults.standard.dictionary(forKey: ITEMS_KEY) ?? [:]
let items = Array(todoDictionary.values)
return items.map({TodoItem(deadline: $0["deadline"] as Date, title: $0["title"] as! String, UUID: $0["UUID"] as! String!)}).sort({(left: TodoItem, right:TodoItem) -> Bool in
(left.deadline.compare(right.deadline) == .orderedAscending)
})
}
TodoItem file
struct TodoItem {
var title: String
var deadline: Date
var UUID: String
init(deadline: Date, title: String, UUID: String) {
self.deadline = deadline
self.title = title
self.UUID = UUID
}
var isOverdue: Bool {
return (Date().compare(self.deadline) == ComparisonResult.orderedDescending)
}
}
First of all, you cannot access a value of a struct using the subscript syntax, you have to use dot syntax, like this: let deadline = todoItem.deadline, where todoItem is an instance of the TodoItem class.
Secondly, you have to cast the retrieved dictionary from UserDefaults to [String:TodoItem], since UserDefaults.dictionary(forKey:) returns a dictionary of type [String:Any].
See the working code below:
func allItems() -> [TodoItem] {
let todoDictionary = UserDefaults.standard.dictionary(forKey: ITEMS_KEY) as? [String:TodoItem] ?? [:]
let items = Array(todoDictionary.values)
return items.map({TodoItem(deadline: $0.deadline, title: $0.title, UUID: $0.UUID)}).sort({(left: TodoItem, right:TodoItem) -> Bool in
(left.deadline.compare(right.deadline) == .orderedAscending)
})
}
I'm trying to use an operator to parse a JSON into an object that conforms with the protocol mappable.
First I have a class called Map that is which it's function is store the current key it's being parsed and the complete JSON to parse and the current value to be stored
private class Map {
var json: [String : Any]?
var key: String!
convenience init(json: [String : Any]?) {
self.init()
self.json = json
}
public var currentValue: Any? {
get{
guard let key = key, let json = json else {
return nil
}
return json[key]
}
}
public subscript(key: String) -> Map {
// save key and value associated to it
self.key = key
return self
}
}
Then I have an operator that get the current value on the rhs based on the Map.Key and passes it to the lhs, and here lies the problem if the lhs is an Array with an object that follows the protocol Mappable, it should use the the operator recursively, until end parsing.
The problem is: the comparison lhs is Array always returns false, so I can't typecast it to do the necessary iteration to parse it. (commented lines).
Any ideas how to approach the problem?
infix operator <<< // JSON to object
private func <<< <T: Any>(lhs: inout T, rhs: Map) {
if let rhsArray = rhs.currentValue as? Array<Dictionary<String, Any>>{
print(type(of: lhs)) // <Array<(Book in *someHash*)>>
debugPrint(lhs is Array<Mappable>) // false
var rhsReturnArray: Array<Mappable> = []
for currentRHSValue in rhsArray {
// let object = T.Element.self.from(Map(json: currentRHSValue))
// rhsReturnArray.append(object)
}
// Do something
}
else if let value = rhs.currentValue as? T{
lhs = value
}
}
private class Autor: Mappable {
var books: [Book]?
func from(map: Map) -> Bool {
books <<< map["Books"]
return true
}
}
private class Book: Mappable {
var title: String?
var numberOfPages: Int?
func from(map: Map) -> Bool {
title <<< map["Title"]
numberOfPages <<< map["Number of Pages"]
return true
}
}
let someDictionary = ["Books" : [["Title": "Happy Potter", "Number of Pages": 420], ["Title": "Happy Potter2", "Number of Pages": 422]]]
private let autor = Autor()
autor.from(map: Map(json: someDictionary))
print(autor.books?.first)
And I can't use third part frameworks on this project.
I am trying to pass a JSON object from the list of returned results to a Model class of User. I want to do all of the assignments / parsing inside of the user object.
I keep getting the message - cannot invoke User with argument of type JSON
Any hints?
let post = JSON(data)
println("The post is: " + post.description)
var user : User
user(post[0])
println(user.getName())
import SwiftyJSON
class User {
var ObjectId = ""
var FirstName = ""
var LastName = ""
var Organization = ""
var CallSign = ""
init(sObjectId : String, sFirstName : String, sLastName : String, sOrganization : String, sCallSign : String)
{
ObjectId = sObjectId
FirstName = sFirstName
LastName = sLastName
Organization = sOrganization
CallSign = sCallSign
}
init(sUser : JSON) {
self.ObjectId = sUser["_id"].string!
self.FirstName = sUser["firstName"].string!
self.LastName = sUser["lastName"].string!
self.Organization = sUser["organization"].string!
}
you have to call the appropriate initializer directly
let post = JSON(data)
println("The post is: " + post.description)
var user = User(sUser: post[0])