In this text, I want to discuss such specific issues with AMA requests on Android as: Handling arrays via a decoder Dynamically extendable data in a response Types mismatch issues Handling arrays via a decoder Let's suppose we have to get a list of users where our presentation layer model will look like this: data class UserItemModel( val id: Int, val fullName: String, val phone: List<String>, val telegram: List<String>, val email: List<String> ) UMA will send us a response: { "field_name": "id", "schema": [ "users_table", "id" ], "type": "integer" }, { "field_name": "fullName", "schema": [ "users_table", "fullName" ], "type": "string" }, { "field_name": "array_agg", "schema": [ "users_table", "array_agg" ], "type": "ARRAY" }, { "field_name": "array_agg", "schema": [ "users_table", "array_agg" ], "type": "ARRAY" }, { "field_name": "array_agg", "schema": [ "emails", "array_agg" ], "type": "ARRAY" } Now let's remember what our basic decoder function looks like: inline fun <reified T> Result.toDecoded(): List<T> { val list = arrayListOf<T>() if (data != null) { for (i in data) { val templateResponse = T::class.constructors.find { it.parameters.isEmpty() }?.call() meta?.forEachIndexed { index, meta -> val fieldValue = i[index] val field: Field = T::class.java.getDeclaredField(meta.field_name) field.isAccessible = true field.set(templateResponse, fieldValue) } templateResponse?.let { list.add(it) } } } return list } As you might have noticed, we may receive fields with identical field names "field_name" : "array_agg" and even totally identical meta as we have for phone and telegram: { "field_name": "array_agg", "schema": [ "users_table", "array_agg" ], "type": "ARRAY" }, However, in our decoder, we parse data by the field name, supposing that it is unique. So we need to handle arrays by the schema name in the email case and by data value check in the phone and telegram case. First of all, let's add some constants: const val ARRAY_AGG = "array_agg" const val USERS_TABLE = "users_table" const val EMAILS = "emails" const val TELEGRAM = "telegram" const val PHONE = "phone" Where we will use the EMAILS, TELEGRAM and PHONE values in our response class like this: data class UserResponse( var id: Int?, var fullName: String, var phone: List<String>?, var telegram: List<String>?, var email: List<String>? ) Now, instead of just: val field: Field = T::class.java.getDeclaredField(meta.field_name) We will add a when statement and add conditions: val field: Field = if (meta.field_name == ARRAY_AGG) { when (meta.schema[0]) { EMAILS -> T::class.java.getDeclaredField(EMAILS) USERS_TABLE -> { val formattedValue = value.toString().replace("[\\[\\],\\s+]".toRegex(), "") if (formattedValue.isDigitsOnly()) T::class.java.getDeclaredField(PHONE) else T::class.java.getDeclaredField(TELEGRAM) } else -> T::class.java.getDeclaredField(meta.field_name) } } else { T::class.java.getDeclaredField(meta.field_name) } Here, we check if the meta's field name is array_agg. If so, we check the first schema’s value. If it's an email, find the same name in the response class. For phone and telegram we have identical meta values. So the question is how to distinguish between them. We know the order. The third field in the response will be the phone number and the fourth telegram, but we cannot check the order since the situation may be the same, but the response may have a different order and number of fields. Since we have to distinguish only 2 fields like phone and telegram, we can do it by checking if one of them consists of only digits. So our next step will be checking if the value equals users_table, then if the data value under this meta consists only of digits. If so, we will search for the PHONE field differently than for TELEGRAM. Dynamically extendable data in response Imagine we have a list of websites with the number of users that were online on certain days (the range we specify in the request, let's say 8.10.2024 - 11.10.2024, so it will return 3 days) And we get the following response: { "result": { "data": [ [ "facebook", 456, 280, 653, 942 ], [ "pinterest", 556, 299, 342, 673 ], ], "meta": [ { "field_name": "website_name", "schema": [ "online_data_tbl", "project_name" ], "type": "straing" }, { "field_name": "day_1", "schema": [ "online_data_tbl", "day_1" ], "type": "int" }, { "field_name": "day_2", "schema": [ "online_data_tbl", "day_2" ], "type": "int" }, { "field_name": "day_3", "schema": [ "online_data_tbl", "day_3" ], "type": "int" }, { "field_name": "total_users", "schema": [ "online_data_tbl", "total_users" ], "type": "int" }, ] } } You can see that we have a separate field for each day containing an int value, not a list of ints. To handle this type of response, we must first of all create a response class: class UsersOnlineResponse { var website_name: String? var total_users: Int? var daysList: MutableList<String>? /** * We need empty constructor for mapping the response on Result .toDecoded() */ constructor() : this("", 0, mutableListOf()) constructor( website_name: String?, total_users: Int?, daysList: MutableList<String> ) { this.website_name = website_name this.total_users = total_users this.daysList = daysList } /** * addDayElement() called on Result .toDecoded() */ fun addDayElement(element: String) { daysList?.add(element) } } We will wait for daysList as for the usual list, but we added the addDayElement function, which extends the list. This function will be called in our decoder. But first, let's add some constants: const val DAY_ELEMENT = "day_" const val ADD_DAY_ELEMENT_FUNC_NAME = "addDayElement" ADD_DAY_ELEMENT_FUNC_NAME in this field we have to store the exact name of our addDayElement function. In the .toDecoded() function we will add a check for DAY_ELEMENT inline fun <reified T> Result.toDecoded(): List<T> { val list = arrayListOf<T>() if (data != null) { for (i in data) { val templateResponse = T::class.constructors.find { it.parameters.isEmpty() }?.call() meta?.forEachIndexed { index, meta -> val fieldValue = i[index] val field: Field = if (meta.field_name == ARRAY_AGG) { when (meta.schema[0]) { EMAILS -> T::class.java.getDeclaredField(EMAILS) USERS_TABLE -> { val formattedValue = value.toString().replace("[\\[\\],\\s+]".toRegex(), "") if (formattedValue.isDigitsOnly()) T::class.java.getDeclaredField( PHONE ) else T::class.java.getDeclaredField(TELEGRAM) } else -> T::class.java.getDeclaredField(meta.field_name) } } else { T::class.java.getDeclaredField(meta.field_name) } field.isAccessible = true field.set(templateResponse, fieldValue) if (meta.field_name.startsWith(DAY_ELEMENT)) { val method = T::class.java.getDeclaredMethod( ADD_DAY_ELEMENT_FUNC_NAME, String::class.java ) method.invoke(templateResponse, fieldValue) } } templateResponse?.let { list.add(it) } } } return list } This is what our final decoder function looks like. Types mismatch issues In UMA responses, we often get improper types. For example, meta says we receive an integer, but we may get double inside, and if in our response class we expect an int but get a float, the value will not be parsed. For enum, we'll receive string values and as for strings, the meta may say that it's a string and a text type. To handle this situation, we’ll add a type check to the decoder. Let's replace our val fieldValue = i[index] with this: // loop and constructor code meta?.forEachIndexed { index, meta -> val value = i[index] val fieldValue = when (meta.type) { "boolean" -> { value as Boolean } "integer" -> { if (value is Double) value.toInt() else value } "string", "text" -> { value as String? } "enum" -> { value as String? } else -> { value } } // rest of the code related to field In this article, we looked at the main issues you might encounter when working with UMA. In future articles, we'll discuss how to work with pagination, sorting, and how to build your own request using table joins. In this text, I want to discuss such specific issues with AMA requests on Android as: Handling arrays via a decoder Dynamically extendable data in a response Types mismatch issues Handling arrays via a decoder Dynamically extendable data in a response Types mismatch issues Handling arrays via a decoder Let's suppose we have to get a list of users where our presentation layer model will look like this: data class UserItemModel( val id: Int, val fullName: String, val phone: List<String>, val telegram: List<String>, val email: List<String> ) data class UserItemModel( val id: Int, val fullName: String, val phone: List<String>, val telegram: List<String>, val email: List<String> ) UMA will send us a response: { "field_name": "id", "schema": [ "users_table", "id" ], "type": "integer" }, { "field_name": "fullName", "schema": [ "users_table", "fullName" ], "type": "string" }, { "field_name": "array_agg", "schema": [ "users_table", "array_agg" ], "type": "ARRAY" }, { "field_name": "array_agg", "schema": [ "users_table", "array_agg" ], "type": "ARRAY" }, { "field_name": "array_agg", "schema": [ "emails", "array_agg" ], "type": "ARRAY" } { "field_name": "id", "schema": [ "users_table", "id" ], "type": "integer" }, { "field_name": "fullName", "schema": [ "users_table", "fullName" ], "type": "string" }, { "field_name": "array_agg", "schema": [ "users_table", "array_agg" ], "type": "ARRAY" }, { "field_name": "array_agg", "schema": [ "users_table", "array_agg" ], "type": "ARRAY" }, { "field_name": "array_agg", "schema": [ "emails", "array_agg" ], "type": "ARRAY" } Now let's remember what our basic decoder function looks like: inline fun <reified T> Result.toDecoded(): List<T> { val list = arrayListOf<T>() if (data != null) { for (i in data) { val templateResponse = T::class.constructors.find { it.parameters.isEmpty() }?.call() meta?.forEachIndexed { index, meta -> val fieldValue = i[index] val field: Field = T::class.java.getDeclaredField(meta.field_name) field.isAccessible = true field.set(templateResponse, fieldValue) } templateResponse?.let { list.add(it) } } } return list } inline fun <reified T> Result.toDecoded(): List<T> { val list = arrayListOf<T>() if (data != null) { for (i in data) { val templateResponse = T::class.constructors.find { it.parameters.isEmpty() }?.call() meta?.forEachIndexed { index, meta -> val fieldValue = i[index] val field: Field = T::class.java.getDeclaredField(meta.field_name) field.isAccessible = true field.set(templateResponse, fieldValue) } templateResponse?.let { list.add(it) } } } return list } As you might have noticed, we may receive fields with identical field names "field_name" : "array_agg" and even totally identical meta as we have for phone and telegram: "field_name" "array_agg" { "field_name": "array_agg", "schema": [ "users_table", "array_agg" ], "type": "ARRAY" }, { "field_name": "array_agg", "schema": [ "users_table", "array_agg" ], "type": "ARRAY" }, However, in our decoder, we parse data by the field name, supposing that it is unique. So we need to handle arrays by the schema name in the email case and by data value check in the phone and telegram case. First of all, let's add some constants: const val ARRAY_AGG = "array_agg" const val USERS_TABLE = "users_table" const val EMAILS = "emails" const val TELEGRAM = "telegram" const val PHONE = "phone" const val ARRAY_AGG = "array_agg" const val USERS_TABLE = "users_table" const val EMAILS = "emails" const val TELEGRAM = "telegram" const val PHONE = "phone" Where we will use the EMAILS , TELEGRAM and PHONE values in our response class like this: EMAILS TELEGRAM PHONE data class UserResponse( var id: Int?, var fullName: String, var phone: List<String>?, var telegram: List<String>?, var email: List<String>? ) data class UserResponse( var id: Int?, var fullName: String, var phone: List<String>?, var telegram: List<String>?, var email: List<String>? ) Now, instead of just: val field: Field = T::class.java.getDeclaredField(meta.field_name) val field: Field = T::class.java.getDeclaredField(meta.field_name) We will add a when statement and add conditions: val field: Field = if (meta.field_name == ARRAY_AGG) { when (meta.schema[0]) { EMAILS -> T::class.java.getDeclaredField(EMAILS) USERS_TABLE -> { val formattedValue = value.toString().replace("[\\[\\],\\s+]".toRegex(), "") if (formattedValue.isDigitsOnly()) T::class.java.getDeclaredField(PHONE) else T::class.java.getDeclaredField(TELEGRAM) } else -> T::class.java.getDeclaredField(meta.field_name) } } else { T::class.java.getDeclaredField(meta.field_name) } val field: Field = if (meta.field_name == ARRAY_AGG) { when (meta.schema[0]) { EMAILS -> T::class.java.getDeclaredField(EMAILS) USERS_TABLE -> { val formattedValue = value.toString().replace("[\\[\\],\\s+]".toRegex(), "") if (formattedValue.isDigitsOnly()) T::class.java.getDeclaredField(PHONE) else T::class.java.getDeclaredField(TELEGRAM) } else -> T::class.java.getDeclaredField(meta.field_name) } } else { T::class.java.getDeclaredField(meta.field_name) } Here, we check if the meta's field name is array_agg . If so, we check the first schema’s value. If it's an email, find the same name in the response class. array_agg For phone and telegram we have identical meta values. So the question is how to distinguish between them. We know the order. The third field in the response will be the phone number and the fourth telegram, but we cannot check the order since the situation may be the same, but the response may have a different order and number of fields. Since we have to distinguish only 2 fields like phone and telegram, we can do it by checking if one of them consists of only digits. We know the order. The third field in the response will be the phone number and the fourth telegram, but we cannot check the order since the situation may be the same, but the response may have a different order and number of fields. Since we have to distinguish only 2 fields like phone and telegram, we can do it by checking if one of them consists of only digits. So our next step will be checking if the value equals users_table , then if the data value under this meta consists only of digits. If so, we will search for the PHONE field differently than for TELEGRAM . users_table PHONE TELEGRAM Dynamically extendable data in response Imagine we have a list of websites with the number of users that were online on certain days (the range we specify in the request, let's say 8.10.2024 - 11.10.2024, so it will return 3 days) And we get the following response: { "result": { "data": [ [ "facebook", 456, 280, 653, 942 ], [ "pinterest", 556, 299, 342, 673 ], ], "meta": [ { "field_name": "website_name", "schema": [ "online_data_tbl", "project_name" ], "type": "straing" }, { "field_name": "day_1", "schema": [ "online_data_tbl", "day_1" ], "type": "int" }, { "field_name": "day_2", "schema": [ "online_data_tbl", "day_2" ], "type": "int" }, { "field_name": "day_3", "schema": [ "online_data_tbl", "day_3" ], "type": "int" }, { "field_name": "total_users", "schema": [ "online_data_tbl", "total_users" ], "type": "int" }, ] } } { "result": { "data": [ [ "facebook", 456, 280, 653, 942 ], [ "pinterest", 556, 299, 342, 673 ], ], "meta": [ { "field_name": "website_name", "schema": [ "online_data_tbl", "project_name" ], "type": "straing" }, { "field_name": "day_1", "schema": [ "online_data_tbl", "day_1" ], "type": "int" }, { "field_name": "day_2", "schema": [ "online_data_tbl", "day_2" ], "type": "int" }, { "field_name": "day_3", "schema": [ "online_data_tbl", "day_3" ], "type": "int" }, { "field_name": "total_users", "schema": [ "online_data_tbl", "total_users" ], "type": "int" }, ] } } You can see that we have a separate field for each day containing an int value, not a list of ints. To handle this type of response, we must first of all create a response class: class UsersOnlineResponse { var website_name: String? var total_users: Int? var daysList: MutableList<String>? /** * We need empty constructor for mapping the response on Result .toDecoded() */ constructor() : this("", 0, mutableListOf()) constructor( website_name: String?, total_users: Int?, daysList: MutableList<String> ) { this.website_name = website_name this.total_users = total_users this.daysList = daysList } /** * addDayElement() called on Result .toDecoded() */ fun addDayElement(element: String) { daysList?.add(element) } } class UsersOnlineResponse { var website_name: String? var total_users: Int? var daysList: MutableList<String>? /** * We need empty constructor for mapping the response on Result .toDecoded() */ constructor() : this("", 0, mutableListOf()) constructor( website_name: String?, total_users: Int?, daysList: MutableList<String> ) { this.website_name = website_name this.total_users = total_users this.daysList = daysList } /** * addDayElement() called on Result .toDecoded() */ fun addDayElement(element: String) { daysList?.add(element) } } We will wait for daysList as for the usual list, but we added the addDayElement function, which extends the list. This function will be called in our decoder. But first, let's add some constants: const val DAY_ELEMENT = "day_" const val ADD_DAY_ELEMENT_FUNC_NAME = "addDayElement" const val DAY_ELEMENT = "day_" const val ADD_DAY_ELEMENT_FUNC_NAME = "addDayElement" ADD_DAY_ELEMENT_FUNC_NAME in this field we have to store the exact name of our addDayElement function. ADD_DAY_ELEMENT_FUNC_NAME addDayElement In the .toDecoded() function we will add a check for DAY_ELEMENT DAY_ELEMENT inline fun <reified T> Result.toDecoded(): List<T> { val list = arrayListOf<T>() if (data != null) { for (i in data) { val templateResponse = T::class.constructors.find { it.parameters.isEmpty() }?.call() meta?.forEachIndexed { index, meta -> val fieldValue = i[index] val field: Field = if (meta.field_name == ARRAY_AGG) { when (meta.schema[0]) { EMAILS -> T::class.java.getDeclaredField(EMAILS) USERS_TABLE -> { val formattedValue = value.toString().replace("[\\[\\],\\s+]".toRegex(), "") if (formattedValue.isDigitsOnly()) T::class.java.getDeclaredField( PHONE ) else T::class.java.getDeclaredField(TELEGRAM) } else -> T::class.java.getDeclaredField(meta.field_name) } } else { T::class.java.getDeclaredField(meta.field_name) } field.isAccessible = true field.set(templateResponse, fieldValue) if (meta.field_name.startsWith(DAY_ELEMENT)) { val method = T::class.java.getDeclaredMethod( ADD_DAY_ELEMENT_FUNC_NAME, String::class.java ) method.invoke(templateResponse, fieldValue) } } templateResponse?.let { list.add(it) } } } return list } inline fun <reified T> Result.toDecoded(): List<T> { val list = arrayListOf<T>() if (data != null) { for (i in data) { val templateResponse = T::class.constructors.find { it.parameters.isEmpty() }?.call() meta?.forEachIndexed { index, meta -> val fieldValue = i[index] val field: Field = if (meta.field_name == ARRAY_AGG) { when (meta.schema[0]) { EMAILS -> T::class.java.getDeclaredField(EMAILS) USERS_TABLE -> { val formattedValue = value.toString().replace("[\\[\\],\\s+]".toRegex(), "") if (formattedValue.isDigitsOnly()) T::class.java.getDeclaredField( PHONE ) else T::class.java.getDeclaredField(TELEGRAM) } else -> T::class.java.getDeclaredField(meta.field_name) } } else { T::class.java.getDeclaredField(meta.field_name) } field.isAccessible = true field.set(templateResponse, fieldValue) if (meta.field_name.startsWith(DAY_ELEMENT)) { val method = T::class.java.getDeclaredMethod( ADD_DAY_ELEMENT_FUNC_NAME, String::class.java ) method.invoke(templateResponse, fieldValue) } } templateResponse?.let { list.add(it) } } } return list } This is what our final decoder function looks like. Types mismatch issues In UMA responses, we often get improper types. For example, meta says we receive an integer, but we may get double inside, and if in our response class we expect an int but get a float, the value will not be parsed. For enum, we'll receive string values and as for strings, the meta may say that it's a string and a text type. To handle this situation, we’ll add a type check to the decoder. Let's replace our val fieldValue = i[index] with this: val fieldValue = i[index] // loop and constructor code meta?.forEachIndexed { index, meta -> val value = i[index] val fieldValue = when (meta.type) { "boolean" -> { value as Boolean } "integer" -> { if (value is Double) value.toInt() else value } "string", "text" -> { value as String? } "enum" -> { value as String? } else -> { value } } // rest of the code related to field // loop and constructor code meta?.forEachIndexed { index, meta -> val value = i[index] val fieldValue = when (meta.type) { "boolean" -> { value as Boolean } "integer" -> { if (value is Double) value.toInt() else value } "string", "text" -> { value as String? } "enum" -> { value as String? } else -> { value } } // rest of the code related to field In this article, we looked at the main issues you might encounter when working with UMA. In future articles, we'll discuss how to work with pagination, sorting, and how to build your own request using table joins.