For example, server response#1 has name Object:
{
"id":"vfa45f42",
"name": {
"firstName":"UserFirstName",
"lastName":"UserLastName"
}
}
But sometimes server response#2 has name String for other objects of user(that's because server has MongoDB, and at v1 it was String, but at v2 it is Object):
{
"id":"abfaf453",
"name":"OneSentenceUserName"
}
So, if I make with response#2 this:
val type = object : TypeToken<User>() {}.type
gson.fromJson(responseString, type)
where
data class User(val id:String, val name: Name)
data class Name(val firstName: String, val lastName: String)
Error is:
java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line ...
I expected to make name = null if it's String at json
Keeping your actual classes User and Name, you can create a custom TypeAdapter and create a full Name out of the single name string or the complete JSON object by reading it yourself.
class NameAdapter : TypeAdapter<Name>() {
#Throws(IOException::class)
override fun read(reader: JsonReader): Name? {
return when(reader.peek()) {
// if { "name": null }
JsonToken.NULL -> {
reader.nextNull()
null
}
// if { "name": "SomeString" }
JsonToken.STRING -> {
Name(reader.nextString(), "")
}
//if { "name": { "firstName": "some", "lastName": "thing" }
JsonToken.BEGIN_OBJECT -> {
var firstName = ""
var lastName = ""
reader.beginObject()
while (reader.hasNext()) {
val peek = reader.peek()
if(peek == JsonToken.END_OBJECT) break
else if(peek == JsonToken.NAME) {
when(reader.nextName()) {
// it will produce an exception if it isn't a string
"firstName" -> firstName = reader.nextString()
"lastName" -> lastName = reader.nextString()
}
}
}
reader.endObject()
Name(firstName, lastName)
}
else -> throw IOException("Unable to parse a name")
}
}
#Throws(IOException::class)
override fun write(writer: JsonWriter, value: Name?) {
if(value == null) {
writer.nullValue()
return
}
writer.beginObject()
writer.name("firstName")
writer.value(value.firstName)
writer.name("lastName")
writer.value(value.lastName)
writer.endObject()
}
}
Then you can add this type adapter to your gson builder.
val gson = GsonBuilder().registerTypeAdapter(Name::class.java, NameAdapter()).build()
It will deserialize correctly the name in each case and produce a full Name class.
Don't know if it will help you directly, but for the same case I did the following with Kotlinx.Serializartion library:
Make name a JsonElement type
val name: JsonElement
Then create custom deserialization function like:
fun getName(): Name? =
if (name is JsonObject) {
// deserialize Name normally
} else {
// create Name by hand, only with 'name' property...
}
Hope this helps.
Found solution, just need to use one of these:
1.
First solution:
data class User(val id:String, #JsonAdapter(FailSafeNameTypeAdapter::class) val name: Name)
where
class FailSafeNameTypeAdapter internal constructor(private val delegate: TypeAdapter<Name>) : TypeAdapter<Name>() {
#Throws(IOException::class)
override fun write(writer: JsonWriter, value: Name?) {
if (value == null) {
writer.nullValue()
return
}
// This catches non-proxied collections AND initialized proxied collections
delegate.write(writer, value)
}
#Throws(IOException::class)
override fun read(reader: JsonReader): Name? {
val peek = reader.peek()
if (peek == STRING) {
reader.skipValue()
return null
}
return delegate.read(reader) as Name
}
}
Second solution
data class User(val id:String, #JsonAdapter(FailSafePriceTypeAdapterFactory::class) val name: Name)
where
class FailSafePriceTypeAdapterFactory : TypeAdapterFactory {
override fun <T> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
if (Price::class.java.isAssignableFrom(type.rawType)) {
val delegate = gson.getDelegateAdapter<T>(this, type) as TypeAdapter<Price>
return FailSafePriceTypeAdapter(delegate) as TypeAdapter<T>
}
return null
}
companion object {
val FACTORY: TypeAdapterFactory = FailSafePriceTypeAdapterFactory()
}
}
3.
Third solution
val gson = GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.IDENTITY)
.apply {
registerTypeAdapterFactory(FailSafePriceTypeAdapterFactory.FACTORY)
}
.create()
Related
I used this example https://baeldung-cn.com/rest-api-search-language-spring-data-querydsl to implement the same in Kotlin.
I created an entity called Shift. Querying is working fine for the equal operators. When comes to the other operators, It throws this error.,
"java.lang.IllegalArgumentException: Unsupported target type : int\n\tat com.querydsl.core.util.MathUtils.cast(MathUtils.java:86)\n\tat com.querydsl.core.types.dsl.NumberExpression.cast(NumberExpression.java:178)\n\tat com.querydsl.core.types.dsl.NumberExpression.goe(NumberExpression.java:293)\n\tat com.presto.salesApp.common.querydsl.ShiftPredicate.getPredicate(ShiftPredicate.kt:19)\n\tat com.presto.salesApp.common.querydsl.ShiftPredicatesBuilder.build$lambda-0(ShiftPredicatesBuilder.kt:34)\n\tat java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:195)\n\tat java.base/java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1654)\n\tat java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484)\n\tat java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474)\n\tat java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:913)\n\tat java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)\n\tat java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:578)\n\tat com.presto.salesApp.common.querydsl.ShiftPredicatesBuilder.build(ShiftPredicatesBuilder.kt:37)\n\tat com.presto.salesApp.modules.shift.ShiftController.getQueryDslShift(ShiftController.kt:166)\n\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\n\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)\n\tat java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\n\tat java.base/java.lang.reflect.Method.invoke(Method.java:566)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:150)\n\tat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)\n\tat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)\n\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1067)\n\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963)\n\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)\n\tat org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898)\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:655)\n\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:764)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197)\n\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:540)\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135)\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357)\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:382)\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:895)\n\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1732)\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)\n\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)\n\tat java.base/java.lang.Thread.run(Thread.java:834)\n",
"message": "Unsupported target type : int",
When I debugged the app, I found that an error is thrown from,
">" -> return path.goe(value)
this line of ShiftPredicate class.
This is my ShiftPredecateBuilder class
package com.presto.salesApp.common.querydsl
import com.querydsl.core.types.Predicate
import com.querydsl.core.types.dsl.BooleanExpression
import com.querydsl.core.types.dsl.Expressions
import java.util.*
import java.util.stream.Collectors
class ShiftPredicatesBuilder {
// val params: List<SearchCriteria> = listOf<SearchCriteria>() ;
val params : MutableList<SearchCriteria> = ArrayList()
// fun ShiftPredicatesBuilder() {
// params = ArrayList<SearchCriteria>();
// }
fun with(
key: String, operation: String, value: Any
): ShiftPredicatesBuilder {
params.add(SearchCriteria(key, operation, value))
return this
}
fun build(): BooleanExpression {
// if (params!!.size == 0) {
// return null;
// }
//
val predicates: MutableList<BooleanExpression> = params
.stream()
.map<BooleanExpression> {
param: SearchCriteria -> ShiftPredicate(param).getPredicate()
}
.filter(Objects::nonNull)
.collect(Collectors.toList())
var result = Expressions.asBoolean(true).isTrue;
for (predicate in predicates) {
result = result.and(predicate as Predicate?)
}
return result;
}
}
This is ShiftPredicate class,
package com.presto.salesApp.common.querydsl
import com.presto.salesApp.modules.shift.Shift
import com.querydsl.core.types.dsl.BooleanExpression
import com.querydsl.core.types.dsl.PathBuilder
class ShiftPredicate(paraCriteria: SearchCriteria) {
private val criteria: SearchCriteria =paraCriteria;
fun getPredicate(): BooleanExpression?
{
val entityPath: PathBuilder<Shift?> = PathBuilder<Shift?>(Shift::class.java, "shift")
if (isNumeric(criteria.value.toString())) {
val path = entityPath.getNumber(criteria.key, Int::class.java)
val value = criteria.value.toString().toInt()
when (criteria.operation) {
":" -> return path.eq(value)
">" -> return path.goe(value)
"<" -> return path.lt(value)
}
} else {
val path = entityPath.getString(criteria.key)
if (criteria.operation.equals(":", ignoreCase = true)) {
return path.containsIgnoreCase(criteria.value.toString())
}
}
return null
}
fun isNumeric(str: String): Boolean {
try {
str.toInt()
} catch (e: NumberFormatException) {
return false
}
return true
}
}
This is the controller method. I hardcoded values for the moment,
#GetMapping("/search")
fun getQueryDslShift(#RequestParam(value = "search") search: String): Any {
val builder = ShiftPredicatesBuilder().with("version",">",3)
val exp: BooleanExpression = builder.build()
return shiftService.getQueryDSLShiftByPredicate(exp)
}
This is the error,
Used this version in POM
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>5.0.0</version>
</dependency>
I faced the same issues before,
Try returning the Int class with javaObjectType, and then its working
on your code, try change this
val path = entityPath.getNumber(criteria.key, Int::class.java)
with this
val path = entityPath.getNumber(criteria.key, Int::class.javaObjectType)
I hope this helps.
I have successfully created few routes overriding configure() method of RouteBuilder. A for loop is used to generate routes on application startup, for eg:
Route1: from("direct:Route1").to("netty-http:http://localhost:8080/route1)
Route2: from("direct:Route2").to("netty-http:http://localhost:8081/route2)
Route3: from("direct:Route3").to("netty-http:http://localhost:8082/route3)
Route4: from("direct:Route4").to("netty-http:http://localhost:8083/route4)
Route5: from("direct:Route5").to("netty-http:http://localhost:8084/route5)
for (endpoint in endpoints.iterator()) {
from("direct:" + endpoint.getEndpointRouteName())
.process(getProcessor(endpoint.getEndpointProcessor(), endpoint.getEndpointRouteName(), objectMapper))
.setHeader(Exchange.HTTP_METHOD, simple(endpoint.getEndpointRequestMethod()))
.setHeader(Exchange.CONTENT_TYPE, constant(endpoint.getEndpointContentType()))
.to("netty-http:" + endpoint.getEndpointUrl())
}
private fun getProcessor(processorClassName: String, name: String, objectMapper: ObjectMapper): Processor {
var processorClass = Class.forName("com.demo.camelpoc.processors.$name.$processorClassName")
return processorClass.getDeclaredConstructor(ObjectMapper::class.java).newInstance(objectMapper) as Processor
}
And there is a source endpoint which starts the workflow. For example the default workflow generated in runtime:
// Workflow
from("netty-http:$sourceUrl").process {
it.setProperty("Workflow", workflowMap)
}
.dynamicRouter(bean(DynamicRouteBean::class.java, "route(*, *, ${startEndpoint})"))
where workflowMap (Used in DynamicRouteBean) is a map of endpoint strings like Key: "direct:Route1 " Value : "direct:Route2", Key: "direct:Route2 " Value : "direct:Route3"... etc
Requirement: Retry sending to the same endpoint in the workflow when exception is thrown in that particular route
For eg:
Lets say, an exception occurs at direct:Route2, I want to retry sending to direct:Route2.
Here is my DynamicRouteBean class.
class DynamicRouteBean {
fun route(
#Header(Exchange.SLIP_ENDPOINT) previousRoute: String?,
exchange: Exchange,
startEndpoint: String
): String? {
if(checkException(exchange)) {
return exchange.getProperty(Exchange.SLIP_ENDPOINT) as String
}
if (exchange.getProperty(Properties.STOP_ROUTE) != null && exchange.getProperty(Properties.STOP_ROUTE) == true) {
return null
}
val workflow: MutableMap<String, Pair<String, String?>> =
exchange.getProperty("Workflow") as MutableMap<String, Pair<String, String?>>
return when (previousRoute) {
null ->
startEndpoint
else -> {
val message = exchange.getIn(NettyHttpMessage::class.java)
// Signifies last endpoint and thus means end of the route
if (!workflow.containsKey(previousRoute)) {
return null
}
if (message?.getHeader(previousRoute.substring(9)) != null) {
val isSuccess = message.getHeader(previousRoute.substring(9)) == true
if (isSuccess) {
"${workflow[previousRoute]?.first}"
} else if (workflow[previousRoute]?.second != null) {
"${workflow[previousRoute]?.second}"
} else {
null
}
} else {
null
}
}
}
}
When I return current Exchange.SLIP_ENDPOINT property as String on exception, it doesn't call that endpoint again but exception message is returned back to the consumer.
Whereas it works in normal cases when there is no exception.
Suggestions would be very helpful on handling this scenario.
I am writing the following Kotlin code but when I compile it gives the error
Null can not be a value of a non-null type ByteArray
this is my code
fun myKafkaProducer(): MessageBusProducer<UUID?, Any>? {
return if (serverProperties().isWAPKafkaSet) {
val uuidSerializer = UUIDSerializer()
val uuidDeserializer = UUIDDeserializer()
val topic = serverProperties().hmsTopic
val serializer = KafkaProperties.Serializers(Function<UUID, ByteArray> { uuid: UUID? -> uuidSerializer.serialize(null, uuid) }, label# Function<Any, ByteArray> { sapMessage: Any ->
try {
return#label ObjectMappers.getObjectMapper().writeValueAsString(sapMessage).toByteArray()
} catch (e: JsonProcessingException) {
e.printStackTrace()
}
null /*showing error here */
}, null,
null
)
// set WapKafkaHostPortAddress in env (configuration repo)
return KafkaMessageBusProducerFactory.Builder<UUID, Any>()
.kafkaAddressPropertyName("portAddress")
.topic(topic)
.messageBusTypeSerializers(serializer)
.build().create(true)
} else {
return null
}
}
I am trying to code the equivalent of Serializers<UUID, Object>
what other datatype can I use ?
The equivalent of Object in Kotlin is the type Any?. Only then are you able to return null, because the type Any is non-nullable and Any? can be null.
I'm trying to setup a prototype for using graphql across multiple java microservices, which requires me to join multiple graphql schema's into one.
I'm using 2 java-services and the ApolloServer with ApolloGateway; which shows the following schema in the playground:
type Client {
id: ID!
name: String
linkeduser: User
}
type Query {
user(id: ID!): User
users: [User]
client(id: ID!): Client
clients: [Client]
}
type User {
id: ID!
name: String
}
When running the simple query:
query client {
client(id: 1) {
id
name
linkeduser {
id
name
}
}
}
What I expect this to return is a client with a linkeduser; When debugging the client service gets queried, the user service gets queried, yet the response is:
{
"data": {
"client": {
"id": "1",
"name": "Bob",
"linkeduser": null
}
}
}
How do I get a linked user response in my client?
I've tried returning lists of users, a new client object with a list of linkedusers, a single user.
The example of https://github.com/apollographql/federation-jvm is the base of this code, though I've yet to see this working.
Code:
Service 1: Client
#WebServlet(loadOnStartup = 1, urlPatterns = "/graphql")
public class GraphQLService extends GraphQLHttpServlet {
#Override
protected GraphQLConfiguration getConfiguration() {
return GraphQLConfiguration.with(getGraphQLSchema()).build();
}
private static GraphQLSchema getGraphQLSchema() {
InputStream inputStream = client.GraphQLService.class
.getClassLoader().getResourceAsStream("schema.graphqls");
TypeDefinitionRegistry parse = new SchemaParser().parse(inputStream);
RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring()
.type("Query", builder -> builder.defaultDataFetcher(GraphQLService::getClient))
.build();
return com.apollographql.federation.graphqljava.Federation.transform(parse, runtimeWiring)
.fetchEntities(env -> env.<List<Map<String, Object>>>getArgument(_Entity.argumentName)
.stream()
.map(values -> {
if ("Client".equals(values.get("__typename"))) {
final Object id = values.get("id");
if (id instanceof String) {
return getSingleClient((String) id);
}
}
return null;
})
.collect(Collectors.toList()))
.resolveEntityType(env -> {
final Object src = env.getObject();
if (src instanceof Client) {
return env.getSchema().getObjectType("Client");
}
return null;
}).build();
}
private static Object getClient(DataFetchingEnvironment environment) {
switch (environment.getFieldDefinition().getName()) {
case "client":
return getSingleClient(environment.getArgument("id"));
case "clients":
return getAllClients();
default:
return null;
}
}
//... extra code with simple getters
}
With this schema :
extend type Query {
client(id: ID!): Client
clients: [Client]
}
type Client #key(fields: "id"){
id: ID!
name: String
}
Service 2: User
#WebServlet(loadOnStartup = 1, urlPatterns = "/graphql")
public class GraphQLService extends GraphQLHttpServlet {
#Override
protected GraphQLConfiguration getConfiguration() {
return GraphQLConfiguration.with(getGraphQLSchema()).build();
}
private static GraphQLSchema getGraphQLSchema() {
InputStream inputStream = user.GraphQLService.class
.getClassLoader().getResourceAsStream("schema.graphqls");
TypeDefinitionRegistry parse = new SchemaParser().parse(inputStream);
RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring()
.type("Query", builder -> builder.defaultDataFetcher(GraphQLService::getUser))
.build();
return com.apollographql.federation.graphqljava.Federation.transform(parse, runtimeWiring)
.fetchEntities(env -> env.<List<Map<String, Object>>>getArgument(_Entity.argumentName)
.stream()
.map(values -> {
if ("Client".equals(values.get("__typename"))) {
final Object id = values.get("id");
if (id instanceof String) {
return getSingleUser((String) id);
}
}
return null;
})
.collect(Collectors.toList()))
.resolveEntityType(env -> {
final Object src = env.getObject();
if (src instanceof User) {
return env.getSchema().getObjectType("User");
}
return null;
})
.build();
}
private static Object getUser(DataFetchingEnvironment environment) {
switch (environment.getFieldDefinition().getName()) {
case "user":
return getSingleUser(environment.getArgument("id"));
case "users":
return getAllUsers();
default:
return null;
}
}
//... extra code with simple getters
}
With this schema :
type Query #extends{
user (id: ID!): User
users: [User]
}
type User #key(fields: "id") {
id: ID!
name: String
}
type Client #key(fields: "id") #extends{
id: ID! #external
linkeduser : User
}
Version in POM.xml
<graphql.version>14.0</graphql.version>
<graphql-tools.version>5.2.4</graphql-tools.version>
<graphql-servlet.version>9.0.1</graphql-servlet.version>
<graphql-federation-support.version>0.4.0</graphql-federation-support.version>
In user service, you need to return a pojo of the type client, with a getter for a linkeduser (only the extends fields need to be present):
if ("Client".equals(values.get("__typename"))) {
final Object id = values.get("id");
if (id instanceof String) {
return new Client((String) id, getSingleUser((String) id));
}
}
Also the resolveTypeEntity needs to resolve to said client
Currently, I use retrofit2 to call restful apis and get response. Because the response body can be multiple types, I wrote the code following.
//Interface
#FormUrlEncoded
#POST("payments/events/{id}")
fun postPayment(#Path("id") id: String): Call<Any>
//Api Manager
fun postPayment(id: String): Observable<Any> {
return Observable.create {
subscriber ->
val callResponse = api.postPayment(id)
val response = callResponse.execute()
if (response.isSuccessful) {
if (response.body() is MyClass1) {
// never success...
} else if (response.body() is MyClass2) {
// never success...
}
subscriber.onNext(response.body())
subscriber.onCompleted()
} else {
subscriber.onError(Throwable(response.message()))
}
}
}
So I'm not able to cast response.body() to MyClass1 or MyClass2.
response.body() as MyClass1 occurs error too.
MyClass1 and MyClass2 are normal template classes.
class MyClass1( val id: String, val data: String)
Is there any smart way to cast response body to my custom classes?
Small update for MyClass2
class MyClass2( val token: String, val url: String, val quantity: Int)
As mentioned by #Miha_x64, Retrofit doesn't know about your classes (MyClass1 and MyClass2) because your Call uses the Any type. Therefore, Retrofit is not creating an instance of MyClass1 or MyClass2, instead it is just creating an instance of the Any class.
The simplest solution would just be to combine the two classes:
data class MyClass(
val id: String?,
val data: String?,
val token: String?,
val url: String?,
val quantity: Int
)
Then you can specify the response type in your interface:
#FormUrlEncoded
#POST("payments/events/{id}")
fun postPayment(#Path("id") id: String): Call<MyClass>
In the case your response does not have an id or data element, they will just be null. Then you can check which type of response was received simply by checking which values are null:
if (response.body().id != null) {
// Handle type 1 response...
} else if (response.body().token != null) {
// Handle type 2 response...
}
A slightly more complex solution would be to write a wrapper for your two classes, and a type adapter to populate the wrapper. This would avoid the nullability of each of the fields, as well as keep your data structure separated.
This would differ based on the ConverterFactory you are using but if, for example, you are using Gson, it would look something like this:
data class ApiResponse(
val val1: MyClass1? = null,
val val2: MyClass2? = null
)
class ApiResponseAdapter : TypeAdapter<ApiResponse> {
#Throws(IOException::class)
override fun write(out: JsonWriter, value: ApiResponse?) {
if (value != null) {
out.beginObject()
value.val1?.id? let { out.name("id").value(it) }
value.val1?.data? let { out.name("data").value(it) }
value.val2?.token? let { out.name("token").value(it) }
value.val2?.url? let { out.name("url").value(it) }
value.val2?.quantity? let { out.name("quantity").value(it) }
out.endObject()
} else {
out.nullValue()
}
}
#Throws(IOException::class)
override fun read(in: JsonReader): ApiResponse {
reader.beginObject()
var id: String? = null
var data: String? = null
var token: String? = null
var url: String? = null
var quantity: Int = 0
while(in.hasNext()) {
val name = in.nextName()
if (name.equals("id", true)) {
id = in.nextString()
} else if (name.equals("data", true)) {
data = in.nextString()
} else if (name.equals("token", true)) {
token = in.nextString()
} else if (name.equals("url", true)) {
url = in.nextString()
} else if (name.equals("quantity", true)) {
quantity = in.nextInt()
}
}
reader.endObject()
if (id != null && data != null) {
return ApiResponse(MyClass1(id, data), null)
} else if (token != null && url != null) {
return ApiResponse(null, MyClass2(token, url, quantity))
} else {
return ApiResponse()
}
}
}
Then you can add this type adapter to your Gson instance:
val gson = GsonBuilder().registerTypeAdapter(ApiResponse::class.java, ApiResponseAdapter()).create()
Then replace the Call<Any> type with Call<ApiRepsone> and you can then check which response was received by checking which value is null:
if (response.body().val1 != null) {
// Handle MyClass1 response...
} else if (response.body().val2 != null) {
// Handle MyClass2 response...
}
First of all, thanks #Bryan for answer. Your answer was perfect but finally I did something tricky way.
...
if (response.isSuccessful) {
val jsonObject = JSONObject(response.body() as Map<*, *>)
val jsonString = jsonObject.toString()
if (jsonObject.has("id")) {
val myclass1Object = Gson().fromJson(jsonString, MyClass1::class.java)
...
} else {
val myclass2Object = Gson().fromJson(jsonString, MyClass2::class.java)
...
}
}
...