I use Retrofit2 and I need to upload various files using file in an array of objects Media like this :
{
"state" = "done",
"medias" = [
{
"file" = THE_FILE1
},
{
"file" = THE_FILE2
},
{
"file" = THE_FILE3
}
]
}
This is the function of my Interface :
#Multipart
#POST("api/exercice/{id}")
fun submitExercice(
#Path("id") id: Int,
#Header("Authorization") token: String,
#Body data: AnswerExercice
): Call<Void>
And this is my object Media :
data class AnswerExercice(
val state: String = "done",
val medias: List<Media>
) : Serializable {
data class Media(
#Part val file: MultipartBody.Part
) : Serializable
}
But I have this error :
#Body parameters cannot be used with form or multi-part encoding.
(parameter #3)
What am i not doing well?
This is what the API documentation say :
The result have to be like this :
Solution 1
If you like to send your data exactly like the structure you mentioned, you should convert files content to Base64 and wrap them in a serializable class and post it as the body. Here is the sample of wrapper class:
data class AnswerExerciceBase64(val state: String, val medias: List<Media>) : Serializable
data class Media(val file: Base64File) : Serializable
class Base64File(file: File) : Serializable {
val name: String
val content: String
init {
name = file.name
content = Base64.encodeToString(FileInputStream(file).readBytes(), Base64.DEFAULT)
}
}
And your Api is like this:
#POST("api/exercice/{id}")
fun submitExercice(
#Path("id") id: Int,
#Header("Authorization") token: String,
#Body data: AnswerExerciceBase64
): Call<Void>
Then posted data to server will be like below:
{
"state": "this is state",
"medias": [{
"file": {
"content": "Base64 file content",
"name": "f1.txt"
}
}, {
"file": {
"content": "Base64 file content",
"name": "f2.txt"
}
}, {
"file": {
"content": "Base64 file content",
"name": "f3.txt"
}
}]
}
This approach is so close to what you want but you should know you must decode files content on the server-side by yourself, so you need more effort on the server-side.
Solution 2
It's better to use multipart/form-data to upload files and data. Based on "Is it possible to have a nested MultipartEntities or FormBodyPart in a multipart POST?" question and its answer, multipart/form-data has a flat structure and there is no hierarchy, so you can't have desired data structure but you can still pass all of the inputs to Api through a single object.
According to this article, you can send multiple files in a List, so if your Api be like this
#Multipart
#POST("post")
fun submitExercice(#Part data: List<MultipartBody.Part>): Call<ResponseBody>
then you will be able to upload multiple files. You just need to create a List of MultipartBody.Part and add your files to it like below:
list.add(MultipartBody.Part.createFormData(name, fileName, RequestBody.create(mediaType, file)))
Now you must add the state parameter to this list. You can do it like this:
list.add(MultipartBody.Part.createFormData("state", state))
I developed a class that handles all this stuff. You can use it.
class AnswerExerciceList(state: String) : ArrayList<MultipartBody.Part>() {
init {
add(MultipartBody.Part.createFormData("state", state))
}
fun addFile(name: String, fileName: String, mediaType: MediaType?, file: File) {
add(MultipartBody.Part.createFormData(name, fileName,
RequestBody.create(mediaType, file)))
}
}
You can create an instance of this class, add your files and then pass it to the submitExercice Api method as input.
Update
This answer is based on your Api documnetation. I tested my answer and the example that you mentioned in your question via https://postman-echo.com and result was the same. Please try the following code snippet:
Api
#Multipart
#POST("api/exercice/{id}")
fun submitExercice(#Path("id") id: Int,
#Header("Authorization") authorization: String,
#Part("answer") answer: String,
#Part medias: List<MultipartBody.Part>,
#Part("state") state: String): Call<ResponseBody>
Media Class
data class Media(val urlVidel: String, val file: File?, val mediaType: MediaType?) {
companion object {
fun mediaListToMultipart(mediaList: List<Media>): List<MultipartBody.Part> {
val list = ArrayList<MultipartBody.Part>()
for (i in mediaList.indices) {
mediaList[i].let {
if (!TextUtils.isEmpty(it.urlVidel))
list.add(MultipartBody.Part.createFormData("medias[$i][urlVideo]", it.urlVidel))
if (it.file != null) {
val requestFile = RequestBody.create(
it.mediaType,
it.file
)
list.add(MultipartBody.Part.createFormData("medias[$i][file]", it.file.getName(), requestFile))
}
}
}
return list
}
}
}
and then call Api like this:
ApiHelper.Instance.submitExercice(1, "Authorization Token", "Answer", Media.mediaListToMultipart(mediaList), "State").enqueue(callback)
Related
I don't understand what is problem clearly. When I searched it in google, I don't decide my reponse model is problem or the json response is problem and should change. Which one? I can't find solution for Kotlin. How I should solve this?
response JSON:
"data":{
"productInfo":{
"data":{
"toBarcode":"2704439285463",
"productJson":{
"p_no":"28420000",
"p_name":"ASA"
}
}
},
"moves":{
"data":[
{
"fisAcik":"MALVERENDEN",
"toBarcode":"2704439285463",
"toJson":{
"to_Hks_Adi":"DAĞITIM MERKEZİ"
},
"movementJson":{
"isleme_Tarihi":"21/12/2022 02:19:30"
}
}
]
}
}
Data.kt
data class Data(
val productInfo: ProductInfo,
val moves: Moves
)
data class Moves (
val data: List<MovesItem>
)
data class MovesItem (
#SerializedName("fisAcik")
val receiptExplanation: String,
val toBarcode: String,
val toJson: ToJson,
val movementJson: MovementJson
)
data class MovementJson (
#SerializedName("isleme_Tarihi")
val processDate: String
)
data class ToJson (
#SerializedName("to_Hks_Adi")
val toUnitHksName: String
)
data class ProductInfo (
val data: ProductInfoItems
)
data class ProductInfoItems (
val toBarcode: String,
val productJson: ProductJson
)
data class ProductJson (
#SerializedName("p_No")
val migrosProductNo: String,
#SerializedName("p_Name")
val migrosProductName: String
)
method that using to call request.
suspend fun dataGetInfo(#Body request: DataRequest): NetworkResult<BaseResponse<Data>>
The framework you are using for this:
...fun dataGetInfo(#Body request: DataRequest)...
is implicitly taking a JSON request and deserializing.
The annotation #SerializedName is a from the Gson library, so I guessed that your framework must be using Gson. From that I was able to test using:
import com.google.gson.Gson
import com.google.gson.annotations.SerializedName
println(Gson().fromJson(src, Data::class.java))
which produces
Data(productInfo=ProductInfo(data=ProductInfoItems(toBarcode=2704439285463, productJson=ProductJson(migrosProductNo=null, migrosProductName=null))), moves=Moves(data=[MovesItem(receiptExplanation=MALVERENDEN, toBarcode=2704439285463, toJson=ToJson(toUnitHksName=DAĞITIM MERKEZİ), movementJson=MovementJson(processDate=21/12/2022 02:19:30))]))
So fundamentally your code is ok, but I think the problem is how the source JSON is "topped and tailed". To get that parse work, I was using
val src = """
{
"productInfo": {
"data": {
"toBarcode": "2704439285463",
"productJson": {
"p_no": "28420000",
"p_name": "ASA"
}
}
},
"moves": {
"data": [
{
"fisAcik": "MALVERENDEN",
"toBarcode": "2704439285463",
"toJson": {
"to_Hks_Adi": "DAĞITIM MERKEZİ"
},
"movementJson": {
"isleme_Tarihi": "21/12/2022 02:19:30"
}
}
]
}
}
"""
Notice how I removed, from your source, "data": since what you pasted is obviously not a JSON document. I guess, therefore, that this is where the problem occurs - something to do with the top or bottom of the JSON document or you need a container object around the JSON for Data
This error was from my wrong request. I saw Ios has same error also when request with wrong value. So, for who will look this quesiton, they should understand it's not from response or kotlin. Check your value it is clearly what request need.
The data that I want to use has this structure:
{
"1": {
"id": 1,
"name": "Bulbasaur"
},
"2": {
"id": 2,
"name": "Ivysaur"
},
"3": {
"id": 3,
"name": "Venusaur"
}
}
Note:
The number labeling each object matches the id of the Pokémon, not the number of Pokémon
My problem is that when I try to create data classes for this it ends up creating a data class for each object. Not one data class that fits each object. I believe this is due to the number labeling the object(Pokémon) being different for each object.
Is there a way I can format this data in maybe one or two data classes and not over 800?
Ideally I would like the data to be structured like this but it does not work when run.
data class ReleasedPokemonModel(
val id: Int,
val name: String
)
When parsing Json to Object with this special case, you should custom Json Deserializer yourself.
Here I use Gson library to parse Json to Object.
First, create a custom Json Deserializer with Gson. As follows:
PokemonResponse.kt
data class PokemonResponse(
val pokemonMap: List<StringReleasedPokemonModel>
)
data class ReleasedPokemonModel(
val id: Int,
val name: String
)
GsonHelper.kt
object GsonHelper {
fun create(): Gson = GsonBuilder().apply {
registerTypeAdapter(PokemonResponse::class.java, PokemonType())
setLenient()
}.create()
private class PokemonType : JsonDeserializer<PokemonResponse> {
override fun deserialize(
json: JsonElement?,
typeOfT: Type?,
context: JsonDeserializationContext?
): PokemonResponse {
val list = mutableListOf<ReleasedPokemonModel>()
// Get your all key
val keys = json?.asJsonObject?.keySet()
keys?.forEach { key ->
// Get your item with key
val item = Gson().fromJson<ReleasedPokemonModel>(
json.asJsonObject[key],
object : TypeToken<ReleasedPokemonModel>() {}.type
)
list.add(item)
}
return PokemonResponse(list)
}
}
}
Next I will create a GsonConverterFactory so that I can addConvertFactory to Retrofit.
val gsonConverterFactory = GsonConverterFactory.create(GsonHelper.create())
And now I will add retrofit.
val retrofit = Retrofit.Builder()
// Custom your Retrofit
.addConverterFactory(gsonConverterFactory) // Add GsonConverterFactoty
.build()
Finally in ApiService, your response will now return type PokemonResponse.
interface ApiService {
#GET("your_link")
suspend fun getGenres(): PokemonResponse
}
The problem is that there's no JSON array there. it's literally one JSON object with each Pokemon listed as a property. I would recommend that you reformat the JSON beforehand to look like this:
[
{
"id": 1,
"name": "Bulbasaur"
},
{
"id": 2,
"name": "Ivysaur"
},
{
"id": 3,
"name": "Venusaur"
}
]
And then you could model it like this:
data class ReleasedPokemonModel(
val id: Int,
val name: String
)
data class Response(
val items: List<ReleasedPokemonModel>
)
See more here.
And see here for discussion about reformatting the data before handing it to Retrofit.
You can use Map to store the key like the following
data class PokemonResponse(
val pokemonMap:Map<String,ReleasedPokemonModel>
)
data class ReleasedPokemonModel(
val id: Int,
val name: String
)
In my application I should upload image to server.
For server requests I used retrofit 2
I write upload codes, but show me error for validation and say me media field is empty.
Upload image request from PostMan : Click to see image
In postman everything is ok and not any problem and image upload successfully!
But in my code show me validation error!
My api interface code :
#Multipart
#POST("media/")
fun uploadImage(
#Header(AUTHORIZATION) auth: String, #Header(ACCEPT) accept: String, #Header(CONTENT_TYPE) contentType: String,
#Part media: MultipartBody.Part
): Single<Response<ResponseModelUploadImage>>
Upload image code :
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
photoEasy.onActivityResult(
requestCode, resultCode
) { thumbnail ->
val imgFile = bitmapToFile(thumbnail, "myImageNameIsThisTest.jpeg")
Log.e("filePath",imgFile.toString())
val reqFile = RequestBody.create(MediaType.parse("multipart/form-data"), imgFile)
val filePart = MultipartBody.Part.createFormData("media", imgFile?.name, reqFile)
presenter.callUploadImage(userToken, APPLICATION_JSON, APPLICATION_JSON, filePart)
}
}
How can I fix it?
URL
This is an example of an upload function in an Android app (in Kotlin) that sends a picture to a server (in this case is a web application developed with Java and Spring Boot) with Retrofit as multipart/form-data:
private suspend fun sendPicture(sessionId: UUID, p: Picture): Boolean {
try {
val data_part = p.data.toRequestBody("multipart/form-data".toMediaTypeOrNull())
val data_multi_part =
MultipartBody.Part.createFormData("picture", p.description, data_part)
val sessionId_part =
sessionId.toString().toRequestBody("multipart/form-data".toMediaTypeOrNull())
val id_part = p.id.toString().toRequestBody("multipart/form-data".toMediaTypeOrNull())
val categoryId_part =
p.categoryId?.toString()?.toRequestBody("multipart/form-data".toMediaTypeOrNull())
val response = api.sendPicture(sessionId_part, id_part, categoryId_part, data_multi_part)
if (!response.isSuccessful) {
#Suppress("BlockingMethodInNonBlockingContext")
val msg = response.errorBody()?.string() ?: textHelper(
R.string.error_send_picture,
p.id.toString()
)
Logger.trace(EventCode.SerializationError, msg, EventSeverity.Error)
}
return response.isSuccessful
} catch (e: Exception) {
Logger.trace(p, EventCode.SerializationError, e.localizedMessage, Action.Send, EventSeverity.Exception)
}
return false
}
And the generated API that uses Retrofit 2.6 is declared as follows:
#Multipart
#POST("SendPicture")
suspend fun sendPicture(#Part("sessionId") sessionId: RequestBody, #Part("pictureId") id: RequestBody, #Part("categoryId") categoryId: RequestBody?, #Part picture: MultipartBody.Part): Response<Void>
Take into account that Picture is an Android Room (ORM) entity and p.data it's the image representation as a byte array (val data: ByteArray).
sessionId = is a session ID (it's just a custom value of type UUID)
pictureId = is the ID of the picture (type: UUID)
categoryId = is the ID of the category the picture belongs to (type: UUID)
These 3 values are custom (you don't need them), they are there just to show how to pass more data to the server together with the image.
The function is written in Kotlin.
I think you have everything you need to extract the code that better suits your needs. This example is taken from the code of a project of mine and it was tested. It sends pictures stored in the local database to the server.
You can easily port the code to Java.
I am working on an android project and using RxAndroid, Retrofit to make API call and retrieve json. The json looks something like following :
{
"result": [
{
"parent": "jhasj",
"u_deviation": "skasks",
"caused_by": "ksks",
"u_owner_mi": {
"link": "https://gddhdd.service-now.com/api/now/v1/table/sys_user/ghtytu",
"value": "ghtytu"
},
"impact": "",
}
]
}
I am using gson to parse the Json. The problem is "u_owner_mi" sometimes reruns empty string "" when there is no value assigned to it. I don't have access to change the return type to null. This is making my app crash as I am expecting an object here.
I get the following error :
Expected BEGIN_OBJECT but was STRING
If you can't modify the server, try replacing the offending line in the server response before passing it to the Gson parser. Something like:
String safeResponse = serverResponse.replace("\"u_owner_mi\": \"\"", "\"u_owner_mi\": null");
Your app (client) code is expecting an object according to a contract specified in the class that you pass to GSON. Your app behaves as it should and crashes loudly. You should consider having your server return "u_owner_mi" : null instead of an empty string, assuming you have control over that. The u_owner_mi field on the client side would have to be a nullable type.
If you don't have the ability to fix the api, you could also write a custom deserializer.
Suppose your result class and sub-object are:
data class Result(
val parent: String,
val owner: Any?
)
data class Owner(
val link: String,
val value: String
)
The deserializer could be:
class ResultDeserializer : JsonDeserializer<Result> {
override fun deserialize(json: JsonElement, typeOfT: Type?, context: JsonDeserializationContext?): Result {
val jsonObject = json.asJsonObject
val ownerProperty = jsonObject.get("owner")
return Result(
parent = jsonObject.get("parent").asString,
owner = if (ownerProperty.isJsonObject) context?.deserialize<Owner>(ownerProperty.asJsonObject, Owner::class.java)
else ownerProperty.asString
)
}
}
Finally, to add the deserializer:
#Test
fun deserialization() {
val gson = GsonBuilder().registerTypeAdapter(Result::class.java, ResultDeserializer()).create()
val result1 = gson.fromJson<Result>(jsonWithObject, Result::class.java)
val result2 = gson.fromJson<Result>(jsonWithEmpty, Result::class.java)
}
I have a request that returns JSON:
{
"success": 0,
"errors": {
"phone": [
"Incorrect phone number"
]
}
}
I plugged Fuel instead of Retrofit for Kotlin. So, my classes are:
data class RegistrationResponse(
val success: Int,
val errors: RegistrationErrorsResponse?) {
class Deserializer : ResponseDeserializable<RegistrationResponse> {
override fun deserialize(content: String): RegistrationResponse? =
Gson().fromJson(content, RegistrationResponse::class.java)
}
}
data class RegistrationErrorsResponse(val phone: List<String>?) {
class Deserializer : ResponseDeserializable<RegistrationErrorsResponse> {
override fun deserialize(content: String): RegistrationErrorsResponse? =
Gson().fromJson(content, RegistrationErrorsResponse::class.java)
}
}
A request looks like:
class Api {
init {
FuelManager.instance.basePath = SERVER_URL
}
fun registration(name: String, phone: String): Request =
"/registration/"
.httpPost(listOf("name" to name, "phone" to phone))
}
private fun register(name: String, phone: String) {
Api().registration(name, phone)
.responseObject(RegistrationResponse.Deserializer()) { _, response, result ->
val registrationResponse = result.component1()
if (registrationResponse?.success == 1) {
showScreen()
} else {
showErrorDialog(registrationResponse?.errors?.phone?.firstOrNull())
}
}
}
A problem is that when error occurs, phone variable in data class (registrationResponse?.errors?.phone) is filled with null, but not "Incorrect phone number".
After searching through Fuel issues I understood that in most cases we don't need to write own deserializators as they are already written by Gson.
In https://github.com/kittinunf/Fuel/issues/265 there is an example. So, just put your data class inside <>:
URL.httpPost(listOf("name" to name, "phone" to phone)).responseObject<RegistrationResponse> ...
and get data through
result.component1()?.errors?.phone?.firstOrNull()
Old version of answer
Probably an obstacle is a list deserialization, see
1. https://github.com/kittinunf/Fuel/issues/233 and
2. https://github.com/kittinunf/Fuel/pull/236.
I think, by default Fuel doesn't use Gson deserialization.
I still don't know how to deserialize a list, But got values with this expression:
((result.component1().obj()["errors"] as JSONObject).get("phone") as JSONArray)[0]