I'm new to Ktor, I come from a Retrofit background, and I want to map this json:
{
"key1": "value1",
"key2": "value2",
...
}
into (actually I don't need to map the json itself only the deserialized version):
[
{"key1": "value1"},
{"key2": "value2"},
...
]
#Serializable
data class MyObject(
val member1: String,
val member2: String
)
The samples I've seen in official docs didn't help much, so I was trying something like:
#InternalSerializationApi
object CustomDeserializer : DeserializationStrategy<List<MyObject>> {
#ExperimentalSerializationApi
override val descriptor = buildSerialDescriptor("MyObject", PolymorphicKind.OPEN){
element("key", String.serializer().descriptor)
element("value", String.serializer().descriptor)
}
#ExperimentalSerializationApi
override fun deserialize(decoder: Decoder): List<MyObject> = decoder.decodeStructure(descriptor) {
val result = ArrayList<MyObject>()
loop# while (true) {
val index = decodeElementIndex(descriptor)
if (index == DECODE_DONE) {
break#loop
} else if (index > 1) {
throw SerializationException("Unexpected index $index")
} else {
result.add(MyObject(decodeStringElement(descriptor, index = 0), decodeStringElement(descriptor, index = 1)))
}
}
return result
}
}
Questions:
Am I in the right path, or there are better ways to achieve it?
How do I add this to my client? (it could be only for a specific request)
ps: this is how I would do it with Gson (ignore the fact this is in Java):
public class MyObjectConverterFactory implements JsonDeserializer<List<MyObject>> {
#Override
public List<MyObject> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
List<MyObject> res = new ArrayList<>();
if (json != null && json.getAsJsonObject() != null) {
JsonObject object = json.getAsJsonObject();
Set<String> keys = object.keySet();
for (String key : keys) {
res.add(new MyObject(key, object.get(key).getAsString()));
}
}
return res;
}
}
As far as I get it from GSON implementation, you need to deserialize from JSON
{"key1":"value1","key2":"value2", ...}
into
listOf(MyObject(member1="key1", member2="value1"), MyObject(member1="key2", member2="value2"), ...)
It's possible with kotlinx.serialization too:
object MyObjectListSerializer : JsonTransformingSerializer<List<MyObject>>(ListSerializer(MyObject.serializer())) {
override fun transformDeserialize(element: JsonElement) =
JsonArray((element as JsonObject).entries.map { (k, v) ->
buildJsonObject {
put("member1", k)
put("member2", v)
}
})
}
Usage (plain kotlinx.serialization):
val result = Json.decodeFromString(MyObjectListSerializer, "{\"key1\":\"value1\",\"key2\":\"value2\"}")
Usage (with Ktor client):
val client = HttpClient {
install(JsonFeature) {
serializer = KotlinxSerializer(Json {
serializersModule = SerializersModule { contextual(MyObjectListSerializer) }
})
}
}
val result = client.get<List<MyObject>>("http://localhost:8000/myObj")
Related
I am new to Kotlin. I get exception when trying to use Mockito's thenAnswer method
Controller:
#RestController
class SampleRestController(
val sampleService: SampleService
) {
#PostMapping(value = ["/sample-endpoint"], consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE])
fun sampleEndpoint(#RequestBody values: List<String>): ResponseEntity<String> {
val response = sampleService.serviceCall(values)
return ResponseEntity.status(HttpStatus.OK).body(response)
}
}
Service:
#Service
#Transactional
class SampleService {
fun serviceCall(values: List<String>): String {
return values.joinToString("")
}
}
Test:
#ExtendWith(MockitoExtension::class)
internal class SampleRestControllerTest {
#Mock
private lateinit var sampleService: SampleService
private lateinit var mockMvc: MockMvc
private lateinit var objectMapper: ObjectMapper
private lateinit var sampleRestController: SampleRestController
#BeforeEach
fun before() {
MockitoAnnotations.initMocks(this)
objectMapper = ObjectMapper()
.registerModule(JavaTimeModule())
.registerKotlinModule()
sampleRestController = SampleRestController(sampleService)
mockMvc = MockMvcBuilders.standaloneSetup(sampleRestController).build()
}
#Test
fun doTest() {
val testData = listOf("123", "456")
//Mockito.`when`(sampleService.serviceCall(testData)).thenReturn("123456")
//Mockito.`when`(sampleService.serviceCall(testData)).thenAnswer { invocation -> "123456" }
Mockito.`when`(sampleService.serviceCall(testData)).thenAnswer { invocation -> {
val numbers = invocation.getArgument<List<String>>(0)
if ("123" == numbers[0] && "456" == numbers[1]) {
"123456"
} else {
"654321"
}
} }
val result: MvcResult = mockMvc.perform(
MockMvcRequestBuilders.post("/sample-endpoint")
.content(objectMapper.writeValueAsString(testData))
.contentType(MediaType.APPLICATION_JSON_VALUE)
)
.andExpect(status().isOk)
.andReturn()
assertEquals("123456", result.response.contentAsString)
}
}
The unit test is working fine when using the thenReturn() and also when using thenAnswer() without any if condition.
When I try to use thenAnswer with if condition then I get classCastException.
Probably because Kotlin only accepts non-null value? How do I resolve this issue.
Check this.
#Test
fun doTest() {
val testData = listOf("123", "456")
//Mockito.`when`(sampleService.serviceCall(testData)).thenReturn("123456")
//Mockito.`when`(sampleService.serviceCall(testData)).thenAnswer { invocation -> "123456" }
Mockito.`when`(sampleService.serviceCall(testData)).thenAnswer(Answer<Any?> { invocationOnMock: InvocationOnMock ->
val values = invocationOnMock.getArgument<List<String>>(0)
if ("123" == values[0] && "456" == values[1]) {
return#Answer "123456"
}
return#Answer "654321"
})
val result: MvcResult = mockMvc.perform(
MockMvcRequestBuilders.post("/sample-endpoint")
.content(objectMapper.writeValueAsString(testData))
.contentType(MediaType.APPLICATION_JSON_VALUE)
)
.andExpect(status().isOk)
.andReturn()
assertEquals("123456", result.response.contentAsString)
}
I am trying to check if the data I get back from the webtestclient is the same as what I expect. But the ZonedDateTime from the User data class is not just shown as a date but as a timestamp while I have applied Jackson to the webtestclient codecs. Example: 2021-12-09T16:39:43.225207700+01:00 is converted to 1639064383.225207700 while I expect nothing to change. Could someone maybe explain what I am doing wrong. (Using this jackson config when calling this endpoint outside of the test gives the date not as timestamp)
WebTestClientUtil:
object WebTestClientUtil {
fun webTestClient(routerFunction: RouterFunction<ServerResponse>): WebTestClient {
return WebTestClient
.bindToRouterFunction(routerFunction)
.configureClient()
.codecs { configurer: ClientCodecConfigurer ->
configurer.defaultCodecs().jackson2JsonEncoder(Jackson2JsonEncoder(objectMapper, MediaType.APPLICATION_JSON))
configurer.defaultCodecs().jackson2JsonDecoder(Jackson2JsonDecoder(objectMapper, MediaType.APPLICATION_JSON))
}
.build()
}
}
Testcase:
#Test
fun `get user when given correct data`() {
val user = GlobalMocks.mockedUser
coEvery { userRepository.getUserWithData(any()) } returns user
val result = webTestClient.get()
.uri("/api/v1/user/${user.userId}")
.exchange()
.expectStatus().is2xxSuccessful
.expectBody<Result>().returnResult().responseBody?.payload
assertEquals(user, result)
}
data class Result(
val payload: User
)
Jackson config:
class JacksonConfig {
companion object {
val serializationDateFormat: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXX")
val deserializationDateFormat: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm[:ss][XXX][X]")
val objectMapper = jacksonObjectMapper().applyDefaultSettings()
private fun ObjectMapper.applyDefaultSettings() =
apply {
disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)
disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)
setSerializationInclusion(JsonInclude.Include.NON_NULL)
registerModule(Jdk8Module())
registerModule(ParameterNamesModule())
registerModule(JsonComponentModule())
registerModule(
JavaTimeModule().apply {
addSerializer(ZonedDateTime::class.java, ZonedDateTimeSerializer(serializationDateFormat))
addDeserializer(ZonedDateTime::class.java, ZonedDateTimeDeserializer())
}
)
}
}
class ZonedDateTimeDeserializer : JsonDeserializer<ZonedDateTime>() {
override fun deserialize(jsonParser: JsonParser, deserializationContext: DeserializationContext): ZonedDateTime {
val epochTime = jsonParser.text.toLongOrNull()
return if (epochTime != null) {
ZonedDateTime.ofInstant(
Instant.ofEpochSecond(epochTime),
currentZone
)
} else {
ZonedDateTime.parse(jsonParser.text, deserializationDateFormat)
}
}
}
}
EDIT: Also found this issue which makes me think that it might have something to do with bindToRouterFunction.
You need to define an ObjectMapper bean so that the auto-configured one is not used:
#Configuration(proxyBeanMethods = false)
class JacksonConfiguration {
companion object {
val serializationDateFormat: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXX")
val deserializationDateFormat: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm[:ss][XXX][X]")
}
#Bean
fun objectMapper() = jacksonObjectMapper().applyDefaultSettings ()
private fun ObjectMapper.applyDefaultSettings() =
apply {
disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)
disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
enable(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)
setSerializationInclusion(JsonInclude.Include.NON_NULL)
registerModule(Jdk8Module())
registerModule(ParameterNamesModule())
registerModule(JsonComponentModule())
registerModule(
JavaTimeModule().apply {
addSerializer(ZonedDateTime::class.java, ZonedDateTimeSerializer(serializationDateFormat))
addDeserializer(ZonedDateTime::class.java, ZonedDateTimeDeserializer())
}
)
}
class ZonedDateTimeDeserializer : JsonDeserializer<ZonedDateTime>() {
override fun deserialize(jsonParser: JsonParser, deserializationContext: DeserializationContext): ZonedDateTime {
val epochTime = jsonParser.text.toLongOrNull()
return if (epochTime != null) {
ZonedDateTime.ofInstant(
Instant.ofEpochSecond(epochTime),
currentZone
)
} else {
ZonedDateTime.parse(jsonParser.text, deserializationDateFormat)
}
}
}
}
I have a problem with the following code snippet:
(Some functions' bodies are ommited for clear view)
fun collectLinks(page: Page): List<String> {
return LinksCrawler().run {
page.accept(this)
this.links
}
}
class LinksCrawler {
private var _links = mutableListOf<String>()
val links
get() = _links.toList()
fun visit(page: Page) { (...) }
fun visit(container: Container) = { (...) }
private fun visit(elements: List<HtmlElement>){ (...) }
}
When I invoke collectLinks() I get
Visitor$LinksCrawler: method 'void ()' not found
(where Visitor is my filename)
As far as I believe, problem would be caused by scope function .run(), maybe that it has no initialisation code that would do sth with LinksCrawler, but correct me if I am wrong.
I do it in .kts file, if it has any meaning. In overall, it is supposed to be an example for a Visitor design pattern. Full file code below:
import Visitor.HtmlElement.Image as Image
import Visitor.HtmlElement.Link as Link
import Visitor.HtmlElement.Table as Table
import Visitor.HtmlElement.Container as Container
main()
// ---------------
fun main() {
val page = Page(Container(Image(), Link(), Image()),
Table(),
Link(),
Container(Table(), Link()),
Container(Image(), Container(Image(), Link())))
println(collectLinks(page))
}
fun collectLinks(page: Page): List<String> {
return LinksCrawler().run {
page.accept(this)
this.links
}
}
class LinksCrawler {
private var _links = mutableListOf<String>()
val links
get() = _links.toList()
fun visit(page: Page) {
visit(page.elements)
}
fun visit(container: Container) = visit(container.elements)
private fun visit(elements: List<HtmlElement>){
for (e in elements) {
when (e) {
is Container -> e.accept(this)
is Link -> _links.add(e.href)
is Image -> _links.add(e.src)
else -> {}
}
}
}
}
fun Container.accept(feature: LinksCrawler) {
feature.visit(this)
}
fun Page.accept(feature: LinksCrawler) = feature.visit(this)
class Page(val elements: MutableList<HtmlElement> = mutableListOf()) {
constructor(vararg elements: HtmlElement) : this(mutableListOf()) {
for (s in elements) {
this.elements.add(s)
}
}
}
sealed class HtmlElement {
class Container(val elements: MutableList<HtmlElement> = mutableListOf()) : HtmlElement() {
constructor(vararg units: HtmlElement) : this(mutableListOf()) {
for (u in units) {
this.elements.add(u)
}
}
}
class Image : HtmlElement() {
val src: String
get() = "http://image"
}
class Link : HtmlElement() {
val href : String
get() = "http://link"
}
class Table : HtmlElement()
}
I'm write a function that should replace an item in map. I have reach it using HashMap but is possible to write something similar in a "kotlinmatic way"?
fun HashMap<Int, String>.ignoreFields(path: String, fieldsToIgnore: FieldsToIgnore) = {
val filtered: List<Field> = fieldsToIgnore.ignoreBodyFields.filter { it.tagFile == path }
filtered.forEach {
val updatedJson = JsonPath.parse(JsonPath.parse(this[it.order])
.read<String>(whatevervariable))
.delete(it.field)
.apply { set("equalJson", this) }
.jsonString()
this.replace(it.order, updatedJson)
}
return this
}
update using map based on answers:
fun Map<Int, String>.ignoreFields(path: String, fieldsToIgnore: FieldsToIgnore): Map<Int, String> {
val filtered = fieldsToIgnore.ignoreBodyFields.filter { it.tagFile == path }
return this.mapValues {m ->
val field = filtered.find { it.order == m.key }
if (field != null) {
JsonPath.parse(JsonPath.parse(this[field.order])
.read<String>(whatevervariable))
.delete(field.field)
.apply { set(pathBodyEqualToJson, this) }
.jsonString()
} else {
m.value
}
}
}
You can use mapValues to conditionally use different value for same key. This will return a new immutable map
Update: filtered will now be a map of order to updatedJson
fun HashMap<Int, String>.ignoreFields(path: String,
fieldsToIgnore: FieldsToIgnore): Map<Int, String> {
val filtered: Map<Int, String> = fieldsToIgnore.ignoreBodyFields
.filter { it.tagFile == path }
.map {
val updatedJson = JsonPath.parse(JsonPath.parse(this[it.order])
.read<String>(whatevervariable))
.delete(it.field)
.apply { set("equalJson", this) }
.jsonString()
it.order to updatedJson
}
return this.mapValues {
filtered.getOrElse(it.key) { it.value }
}
}
A possible solution is to use mapValues() operator, e.g.:
fun Map<Int, String>.ignoreFields(ignoredFields: List<Int>): Map<Int, String> {
return this.mapValues {
if (ignoredFields.contains(it.key)) {
"whatever"
} else {
it.value
}
}
}
// Example
val ignoredFields = listOf<Int>(1,3)
val input = mapOf<Int, String>(1 to "a", 2 to "b", 3 to "c")
val output = input.ignoreFields(ignoredFields)
print(output)
// prints {1=whatever, 2=b, 3=whatever}
I'm trying to add url values (imgURL variable) taken from JSON into an ArrayList with Kotlin, but somehow when in the scope of the for loop it doesn't seem to work regardless of having my newArrayURLs array declared as a function variable. Any ideas why is this?
private fun getJsonObjectImg(Url: String): ArrayList<String>{
var paths: ArrayList<String> =ArrayList()
val params = HashMap<String, String>()
var newArrayURLs = ArrayList<String>()
val stringRequest = object : StringRequest(Request.Method.POST, Url, Response.Listener { s ->
try {
val array = JSONObject(s)
var imagesArray = array.getJSONArray("images")
for(i in 0..imagesArray.length() -1 ){
var imgURL = imagesArray.getJSONObject(i).getString("fileName")
newArrayURLs.add(imgURL)
paths.add(imgURL)
}
Log.d("pathsin TRY", paths.count().toString())
} catch(e:JSONException){
Log.d("ERROR------", e.toString())
null
}
}, Response.ErrorListener {
error: VolleyError? ->
try {
Log.d("ERROR======","V")
} catch (e: JSONException){
e.printStackTrace()
}
}) {
override fun getParams(): Map<String, String> = mapOf("uploadedBy" to id)
}
val requestQueue = Volley.newRequestQueue(this)
requestQueue.add<String>(stringRequest)
return newArrayURLs
}