Kotlin - Deserializing with Jackson XmlMapper - java

I'm having some trouble to parse correctly an XML file with Jackson XML Mapper.
Suppose I have this simple XML:
<Test>
<Simple>
<x>I am x</x>
<y>I am y</y>
</Simple>
</Test>
and this simple class:
#Root(name="Simple",strict = false)
class Simple {
#Element(name = "x", required = false)
lateinit var x :String
#Element(name = "y", required = false)
lateinit var y :String
}
The compiler keeps throwing this error:
Exception in thread "main" com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "Simple" (class Simple), not marked as ignorable (2 known properties: "x", "y"])
I guess it's because Kotlin can't find the proper structure of the file within the class?
Problem is I need to parse a much more big xml file, and need to avoid to mirror every single Element of the xml, otherwise it will become huge.
So how can I "partially" parse the xml, setting the root to the <Simple> tag, avoiding the <Test> tag?
Thank you so much!

Here's a working solution. Note that I've made the mapper case insensitive to cope with the capitalised Simple property in the XML. You could do this with an annotation on the property instead to override the default lower case name. Note also the use of data classes that do away with any need to use lateinit vars or suchlike:
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.MapperFeature
import com.fasterxml.jackson.dataformat.xml.JacksonXmlModule
import com.fasterxml.jackson.dataformat.xml.XmlMapper
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
data class Simple(val x: String, val y: String)
data class Test(val simple: Simple)
fun main() {
val xmlDeserializer = XmlMapper(JacksonXmlModule().apply {
setDefaultUseWrapper(false)
}).registerKotlinModule()
.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
val simple = Simple("x","y")
val test = Test(simple)
println(xmlDeserializer.writeValueAsString(test))
val stringToParse = "<Test><Simple><x>1</x><y>2</y></Simple></Test>"
val finalObject = xmlDeserializer.readValue(stringToParse, Test::class.java)
println(finalObject.simple.x)
}
make sure your build.gradle.kts has these dependencies if they aren't there already:
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1")
implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.13.1")

Related

Decode generic type with Jackson

Jackson doesn't attempt to decode generic elements of a class.
How do you pass in the the type information without creating a specific version of the class?
The data types are:
#JsonIgnoreProperties(ignoreUnknown = true)
data class HasuraTriggerPayload<T>(
val event: HasuraEvent<T>,
#JsonProperty("created_at")
val createdAt: Instant,
val trigger: Trigger
)
#JsonIgnoreProperties(ignoreUnknown = true)
data class HasuraEvent<T>(
val op: String,
val data: HasuraData<T>
)
data class HasuraData<T>(
val old: T,
val new: T
)
data class Trigger(
val name: String
)
data class Price(
val id: UUID,
val price: BigDecimal?,
)
My route is something like:
post("/price") {
val payload = call.receive<HasuraTriggerPayload<Price>>()
// etc
}
Which produces this error because it doesn't know how to decode new/old:
java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class com.scraptickets.rest.models.Price (java.util.LinkedHashMap is in module java.base of loader 'bootstrap'; com.scraptickets.rest.models.Price is in unnamed module of loader 'app')
trigger.name gives the type. in this case, it's "price-trigger"
There are two possible solutions to this.
We know that T is Price at our particular call site, so we could somehow pass along that information, but how?
We can determine the type of old and new from trigger.name, but how do we write that? The only obvious way is from reading a field of the object we're trying to determine the class for, not from outside.
After a bunch of searching, I found the solution.
You need to create a TypeReference.
For my use the solution was:
post("/price") {
val typeRef = object : TypeReference<HasuraTriggerPayload<Price>>() { }
val objectMapper = jacksonObjectMapper() // Not quite this, I'm leaving out the part to handle timestamps
val jsonStr = call.receiveText()
val payload = objectMapper.readValue(jsonStr, typeRef)
// etc
}

YamlSlurper cannot parse Enum value

I have yaml:
- flowId: "2021:6:10:20:22:7"
flowType: "BIG"
summary: "Description"
flowStatus: "NEW"
createdDate: "2021-06-10"
lastModifiedDate: "2021-06-10"
class Flow{
...
FlowType flowType;
...
}
Enum FlowType{
SMALL,MEDIUM, BIG;
}
Parsing file using YamlSlurper:
def flowList = new YamlSlurper().parseText(new File(myFile).text).collect { it as Flow }
error: java.lang.ClassCastException: java.lang.String cannot be cast to model.FlowType
Is there a way to solve this?
The YAML slurper is a cute tool to quickly read a YAML file or string
and deal with it to the degree, that you would use the other slurpers:
get some basic data types inside lists and maps and just use that.
Your attempt to cast the map to the object only works for very basic
objects. The cast basically gets unrolled to something like:
[a: 42] as X
becomes
def x = new X()
map.each{ k, v ->
x."$k" = v
}
This does:
not coerce/convert types
fails if keys in the map, that are not set-able properties in the
resulting object
If you need proper object mapping, the slurpers are not directly useful
most of the time. You would rather switch to using something made for
that task -- e.g. like Jackson.
Lucky for us, the YAML slurper just uses Jackson (it actually just
transforms the YAML into JSON and then uses the JSON slurper to give you
your data back).
Following an example of how to load the YAML data into objects using
Jackson (all deps are already there, if you already see YamlSlurper):
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper
def yamlStr = """\
- flowId: "2021:6:10:20:22:7"
flowType: "BIG"
summary: "Description"
flowStatus: "NEW"
createdDate: "2021-06-10"
lastModifiedDate: "2021-06-10"
"""
enum FlowType{ SMALL,MEDIUM, BIG }
#groovy.transform.ToString
class Flow {
FlowType flowType
}
def flows = new ObjectMapper(new YAMLFactory()).with{
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
readValue(yamlStr, new TypeReference<List<Flow>>(){})
}
println flows
// → Flow(BIG)

Json to object deserialization issue in Graphql-spqr

Json to GraphQLArgumetn object conversion failing in graphql-spqr.
tried adding GraphQLInterface(with autodiscovery true and scanpackage) to above abstract classes
and GraphQLtype type all concrete classes.
My graph query:
query contactsQuery($searchQuery : QueryInput) { contacts(searchQuery:$searchQuery){id}}
variables:{"searchQuery":{"bool":{"conditions":[{"must":{"matches":[{"singleFieldMatch":{"boost":null,"field":"firstname","value":"siddiq"}}],"bool":null}}]}})
Java code:
#JsonTypeInfo(use=JsonTypeInfo.Id.NAME,include=JsonTypeInfo.As.WRAPPER_OBJECT)
#JsonSubTypes({#type(value = Must.class, name="must"),#type(value = MustNot.class, name="mustNot")})
public abstract class Condition
#JsonTypeInfo(use=JsonTypeInfo.Id.NAME,include=JsonTypeInfo.As.WRAPPER_OBJECT)
#JsonSubTypes({#type(value = SingleFieldMatch.class, name="singleFieldMatch"),#type(value = MultiFieldMatch.class, name="multiFieldMatch")})
public abstract class Match
#GraphQLQuery(name = "contacts")
public List getContacts(#GraphQLArgument(name ="searchQuery") Query query)
Still it's throwing error unknown field error etc. Not sure which configuration is missing.
Building GraphQLSchema with AnnotatedResolvedBuilder, base package configured JacksonValueMappperFactory and singleton services.
Hi this may be a similar issue to what I ended up having.
Initially I had the following
#JsonTypeInfo(use = Id.NAME, include = As.PROPERTY, property = "type")
#GraphQLInterface(name = "AbstractClass", implementationAutoDiscovery = true)
public abstract class AbstractClass{
with the following query called
addNewObject(object: {name: "soft2", id: "asdas"})
To get conversion functioning what I needed to do was the following change
#JsonTypeInfo(use = Id.NAME, include = As.EXISTING_PROPERTY, property = "type")
#GraphQLInterface(name = "AbstractClass", implementationAutoDiscovery = true)
public abstract class AbstractClass{
private String type = this.getClass().getSimpleName();
/**
* #return the type
*/
#GraphQLQuery(name = "type", description = "The concrete type of the node. This should match the initialised class. E.g. \"Concrete\", \"DecafCoffee\"")
public String getType() {
return type;
}
with the query now being
addNewConcreteObject(concrete: {name: "soft2", id: "asdas", type: "Concrete"})
Why this worked (I think):
When converting from JSON to objects in my code using the Jackson converter (ObjectMapper). I had previously noticed that the JSON required knowledge of what class to convert to. Thus the initial use of #JsonTypeInfo(use = Id.NAME, include = As.PROPERTY, property = "type") put a type property in the JSON when it was written to string.
The inclusion of the #JSON tag may be picked up by SPQR and it then seems to use a Jackson converter to try to convert your query to the required object.
If I am right, here is the issue.
As the query doesn't contain type the query can not be correctly converted. Moreover as the type property was not a member variable of the object but was instead only added by the ObjectMapper, SPQR didn't pick it up and so it wasn't part of the schema for the object. Thus to get around it, I added type as a member variable which is always equal to the actual class, then changed my JsonTypeInfo to look for an existing property.
I appreciate this isn't a direct answer to your question (and definitely isn't a pretty answer), but hopefully it will help you find your solution.

How to unmarshall XML with default value as empty string using XStream?

I am trying to figure out how to unmarshall XML when a tag is missing, I can set the default value as empty string instead of NULL. Currently XStream is using null, which is not what I want.
This class has like over 40 properties, which are all String. There is a constructor with default value for each. I mean, like this:
case class MyData(id: String = "", b: String = "", ....)
(yes, I am trying to use it with Scala)
Technically I could write a custom converter that sets them as empty string, but that feels a little silly.
I tried using this
new XStream(new PureJavaReflectionProvider())
as suggested from here: https://stackoverflow.com/a/29747705/598562
It doesn't seem to work though.
any other idea?
XStreams uses a default, empty constructor and then follows it up by calling setters afterwards. So, to get this to work without a custom converter then you will need to create an explicit empty constructor which fills everything in with the defaults you expect. Here is a sample, working application:
import com.thoughtworks.xstream.XStream
import com.thoughtworks.xstream.converters.reflection.PureJavaReflectionProvider
object Hello extends App {
val xstream = new XStream(new PureJavaReflectionProvider())
xstream.alias("MyData", classOf[MyData])
val output = xstream.fromXML("<MyData><id>This should fill in</id></MyData>")
println(output)
}
case class MyData(id: String = "", var b: String = "")
{
def this() = this("", "")
}

Using Jackson to (De)-serialize a Scala Case Class

I tested out the serialization of a Scala case class using Jackson.
DeserializeTest.java
public static void main(String[] args) throws Exception { // being lazy to catch-all
final ObjectMapper mapper = new ObjectMapper();
final ByteArrayOutputStream stream = new ByteArrayOutputStream();
mapper.writeValue(stream, p.Foo.personInstance());
System.out.println("result:" + stream.toString());
}
}
Foo.scala
object Foo {
case class Person(name: String, age: Int, hobbies: Option[String])
val personInstance = Person("foo", 555, Some("things"))
val PERSON_JSON = """ { "name": "Foo", "age": 555 } """
}
When I ran the above main of the Java class, an exception was thrown:
[error] Exception in thread "main" org.codehaus.jackson.map.JsonMappingException:
No serializer found for class p.Foo$Person and no properties discovered
to create BeanSerializer (to avoid exception,
disable SerializationConfig.Feature.FAIL_ON_EMPTY_BEANS) )
How can I (de)-serialize Scala case classes?
Jackson is expecting your class to be a JavaBean, which means its expects the class to have a getX() and/or setX() for every property.
Option 1
You can create JavaBean classes in Scala using the annotation BeanProperty.
Example
case class Person(
#BeanProperty val name: String,
#BeanProperty val age: Int,
#BeanProperty val hobbies: Option[String]
)
In this case a val will mean only a getter is defined. If you want setters for deserialization you defined the properties as var.
Option 2
While option 1 will work, if you really want to use Jackson there are wrappers that allow it to deal with Scala classes like FasterXML's scala module which might be a better approach. I haven't used it as I've just been using the Json library built in to play.
Found a solution that works with jackson and scala case classes.
I used a scala module for jackson - jackson-module-scala.
libraryDependencies ++= Seq(
"com.fasterxml.jackson.core" % "jackson-databind" % "2.5.3",
"com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.2.2"
)
I had to annotate fields in my case class with #JsonProperty.
This is what my case class looks like:
case class Person(#JsonProperty("FName") FName: String, #JsonProperty("LName") LName: String)
And this is how I deserialize:
val objectMapper = new ObjectMapper() with ScalaObjectMapper
objectMapper.registerModule(DefaultScalaModule)
val str = """{"FName":"Mad", "LName": "Max"}"""
val name:Person = objectMapper.readValue[Person](str)
Serialization is easier:
val out = new ByteArrayOutputStream()
objectMapper.writeValue(out, name)
val json = out.toString
Would like to clarify that I am using
com.fasterxml.jackson.databind.ObjectMapper
In the question, it seems he is using
org.codehaus.jackson.map.ObjectMapper
which won't work with ScalaObjectMapper.
Based on Priyank Desai's answer I have created a generic function to convert json string to case class
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import com.fasterxml.jackson.module.scala.experimental.ScalaObjectMapper
def jsonToType[T](json:String)(implicit m: Manifest[T]) :T = {
val objectMapper = new ObjectMapper() with ScalaObjectMapper
objectMapper.registerModule(DefaultScalaModule)
objectMapper.readValue[T](json)
}
Usage:
case class Person(#JsonProperty("name") Name:String, #JsonProperty("age") Age:Int)
val personName = jsonToType[Person](jsonString).name
Deserializing objects is way easier with upickle than Jackson. Here's an example:
case class City(name: String, funActivity: String, latitude: Double)
implicit val cityRW = upickle.default.macroRW[City]
val str = """{"name":"Barcelona","funActivity":"Eat tapas","latitude":41.39}"""
val barcelona = upickle.default.read[City](str)
The Jackson solutions are way more verbose. See this post to learn more about this approach.
I have created a generic function to convert JSON String to Case Class/Object and Case Class/Object to JSON String.
Please find a working and detailed answer which I have provided using generics here.
If you are using Spring MVC, use a configuration like this:
import java.util.List
#Configuration
#EnableWebMvc
class WebConfig extends WebMvcConfigurer {
override def configureMessageConverters(converters: List[HttpMessageConverter[_]]): Unit = {
val conv = new MappingJackson2HttpMessageConverter();
val mapper = JsonMapper.builder()
.addModule(DefaultScalaModule)
.build();
conv.setObjectMapper(mapper);
converters.add(conv);
}
}
Then serializing / deserializing plain case classes and Collection types will work as expected.

Categories

Resources