currently I'm designing a domain model for an application. I created a simple value object that's basically just a wrapper around a string enhanced with some business logic.
Now the default behaviour of jackson is to render the object like
"routerId": {
"routerId": "aa:aa:aa:aa:aa:aa"
}
for
#Embeddable
data class RouterId(val routerId: String) {
init {
val octets = routerId.split(":")
if (octets.size != 6) {
throw IllegalArgumentException("$routerId does not consist of 6 octets")
}
for (octet in octets) {
Integer.parseInt(octet, 16)
}
}
}
I stumbeld accross http://docs.spring.io/spring-data/rest/docs/2.6.3.RELEASE/reference/html/#_adding_custom_de_serializers_to_jackson_s_objectmapper and tried to supply my custom jackson module to handle serialization with
class NicemediaModule : SimpleModule("NicemediaModule") {
override fun setupModule(context: SetupContext?) {
val serializers = SimpleSerializers()
serializers.addSerializer(RouterId::class.java, RouterIdSerializer())
context?.addSerializers(serializers)
}
}
private class RouterIdSerializer : StdSerializer<RouterId>(RouterId::class.java) {
override fun serialize(value: RouterId?, gen: JsonGenerator?, provider: SerializerProvider?) {
gen?.writeString(value?.routerId)
}
}
and
#Configuration
open class SpringDataRestConfiguration : RepositoryRestConfigurerAdapter() {
override fun configureJacksonObjectMapper(objectMapper: ObjectMapper?) {
objectMapper?.registerModule(NicemediaModule())
}
}
but this only leads to
"routerId": {
"content": "aa:aa:aa:aa:aa:aa"
}
Could anyone point out what I would have to do to serialize the RouterId just to a plain string like "routerId": "aa:aa:aa:aa:aa:aa"?
Edit:
I added #Component to my SimpleModule so that Spring Boot loads it by default and wrote a litte test to see if the ObjectMapper works.
#SpringBootTest
#RunWith(SpringRunner::class)
class JsonSerializationTest {
#Autowired
private lateinit var mapper: ObjectMapper
#Test
fun serializeRouterId() {
val routerId: String = "11:11:11:11:11:11"
assertEquals("\"$routerId\"", mapper.writeValueAsString(RouterId(routerId)))
}
}
works quite fine. This may be an indicator that my code is working the whole time but Spring Data REST fails to serialize my model at some point.
Try implementing a custom BackendIdConverter SPI as suggested in this answer. Works like a charm!
Related
I'm trying to get a Kotlin JS app working, and when consuming data from a server using this code:
val client = HttpClient {
install(JsonFeature) {
serializer = KotlinxSerializer()
}
}
#Serializable
data class Entry(val start: String, val end: String)
suspend fun loadData() {
val data = client.get<List<Entry>>("http://localhost:8080/data") {
accept(ContentType.Application.Json)
}
console.log(data)
}
I get exceptions like this:
Serializer for class 'Entry' is not found.
Mark the class as #Serializable or provide the serializer explicitly.
On Kotlin/JS explicitly declared serializer should be used for interfaces and enums without #Serializable annotation
even though the class is marked as #Serializable.
If I change it to client.get<List<Map<String,String>>> then I get a valid result.
What am I doing wrong?
I needed to add this to build.gradle.kts:
kotlin("plugin.serialization") version "1.4.31"
How can we use kotlin.serialize with Ktor's HttpClient to deserialize/serialize JSON with lists as root? I am creating the HttpClient as follows:
HttpClient {
install(JsonFeature) {
serializer = KotlinxSerializer().apply {
setMapper(MyClass::class, MyClass.serializer())
setMapper(AnotherClass::class, AnotherClass.serializer())
}
}
install(ExpectSuccess)
}
Appears I need to setMapper for List, however that is not possible with generics. I see I can get the serializer for it with MyClass.serializer().list, but registering it to deserialize/serialize on http requests is not straight forward. Anyone know of a good solution?
You can write wrapper and custom serializer:
#Serializable
class MyClassList(
val items: List<MyClass>
) {
#Serializer(MyClassList::class)
companion object : KSerializer<MyClassList> {
override val descriptor = StringDescriptor.withName("MyClassList")
override fun serialize(output: Encoder, obj: MyClassList) {
MyClass.serializer().list.serialize(output, obj.items)
}
override fun deserialize(input: Decoder): MyClassList {
return MyClassList(MyClass.serializer().list.deserialize(input))
}
}
}
Register it:
HttpClient {
install(JsonFeature) {
serializer = KotlinxSerializer().apply {
setMapper(MyClassList::class, MyClassList.serializer())
}
}
}
And use:
suspend fun fetchItems(): List<MyClass> {
return client.get<MyClassList>(URL).items
}
Update with ktor 1.3.0:
Now you're able to receive default collections(such a list) from the client directly:
#Serializable
data class User(val id: Int)
val response: List<User> = client.get(...)
// or client.get<List<User>>(...)
Before ktor 1.3.0:
There is no way to (de)serialize such JSON in the kotlinx.serialization yet.
For serialization you could try something like this:
fun serializer(data: Any) = if (data is List<*>) {
if (data is EmptyList) String::class.serializer().list // any class with serializer
else data.first()::class.serializer().list
} else data.serializer()
And there are no known ways to get the list deserializer.
This is more of a workaround but after stepping through KotlinxSerializer code I couldn't see any other way round it. If you look at KotlinxSerializer.read() for example you can see it tries to look up a mapper based on type but in this case it's just a kotlin.collections.List and doesn't resolve. I had tried calling something like setListMapper(MyClass::class, MyClass.serializer()) but this only works for serialization (using by lookupSerializerByData method in write)
override suspend fun read(type: TypeInfo, response: HttpResponse): Any {
val mapper = lookupSerializerByType(type.type)
val text = response.readText()
#Suppress("UNCHECKED_CAST")
return json.parse(mapper as KSerializer<Any>, text)
}
So, what I ended up doing was something like (note the serializer().list call)
suspend fun fetchBusStops(): List<BusStop> {
val jsonArrayString = client.get<String> {
url("$baseUrl/stops.json")
}
return JSON.nonstrict.parse(BusStop.serializer().list, jsonArrayString)
}
Not ideal and obviously doesn't make use of JsonFeature.
I happened to have the same problem on Kotlin/JS, and managed to fix it this way:
private val client = HttpClient(Js) {
install(JsonFeature) {
serializer = KotlinxSerializer().apply {
register(User.serializer().list)
}
}
}
...
private suspend fun fetchUsers(): Sequence<User> =
client.get<List<User>> {
url("$baseUrl/users")
}.asSequence()
Hope this helps :)
I'm currently working on a project where I need to fetch a large amount of data from the Database and parse it into a specific Json format, I already have built my custom Serializers and Its working properly when i pass a List to Gson. But as I was already working with Streams from my JPA Layer, I thought I could pass the Stream down to the Gson parser so that it could transform it directly to my Json data. But I'm getting an empty Json object instead of a correctly populated one.
So, if anyone could point to me a way to make Gson work with Java 8 Streams or if this isn't possible currently.. i could not find anything on Google, so i came to Stackoverflow.
You could use JsonWriter to streaming your data to output stream:
public void writeJsonStream(OutputStream out, Stream<DataObject> data) throws IOException {
try(JsonWriter writer = new JsonWriter(new OutputStreamWriter(out, "UTF-8"))) {
writer.setIndent(" ");
writer.beginArray();
data.forEach(d -> {
d.beginObject();
d.name("yourField").value(d.getYourField());
....
d.endObject();
});
writer.endArray();
}
}
Note that you're in charge of controling the json structure.
That is, if your DataObject contains nested Object, you have to write beginObject()/endObject() respectively. The same goes for nested array.
It is not as trivial as one would expect, but it can be done in a generic way.
When you look into the Javadoc to TypeAdapterFactory, they provide a very simplistic way of writing a TypeAdapterFactory for a custom type. Alas, it does not work as expected because of problems with element type detection. The proper way to do this can be found in Gson-internal CollectionTypeAdapterFactory. It is quite complex, but taking what's necessary one can come up with something like that:
final class StreamTypeAdapterFactory implements TypeAdapterFactory {
#SuppressWarnings("unchecked")
#Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
Type type = typeToken.getType();
Class<? super T> rawType = typeToken.getRawType();
if (!Stream.class.isAssignableFrom(rawType)) {
return null;
}
Type elementType = ExtraGsonTypes.getStreamElementType(type, rawType);
TypeAdapter<?> elementAdapter = gson.getAdapter(TypeToken.get(elementType));
return (TypeAdapter<T>) new StreamTypeAdapter<>(elementAdapter);
}
private static class StreamTypeAdapter<E> extends TypeAdapter<Stream<E>> {
private final TypeAdapter<E> elementAdapter;
StreamTypeAdapter(TypeAdapter<E> elementAdapter) {
this.elementAdapter = elementAdapter;
}
public void write(JsonWriter out, Stream<E> value) throws IOException {
out.beginArray();
for (E element : iterable(value)) {
elementAdapter.write(out, element);
}
out.endArray();
}
public Stream<E> read(JsonReader in) throws IOException {
Stream.Builder<E> builder = Stream.builder();
in.beginArray();
while (in.hasNext()) {
builder.add(elementAdapter.read(in));
}
in.endArray();
return builder.build();
}
}
private static <T> Iterable<T> iterable(Stream<T> stream) {
return stream::iterator;
}
}
The ExtraGsonTypes is a special class that I used to circumvent package-private access to $Gson$Types.getSupertype method. It's a hack that works if you're not using JDK 9's modules - you simply place this class in the same package as $Gson$Types:
package com.google.gson.internal;
import java.lang.reflect.*;
import java.util.stream.Stream;
public final class ExtraGsonTypes {
public static Type getStreamElementType(Type context, Class<?> contextRawType) {
return getContainerElementType(context, contextRawType, Stream.class);
}
private static Type getContainerElementType(Type context, Class<?> contextRawType, Class<?> containerSupertype) {
Type containerType = $Gson$Types.getSupertype(context, contextRawType, containerSupertype);
if (containerType instanceof WildcardType) {
containerType = ((WildcardType)containerType).getUpperBounds()[0];
}
if (containerType instanceof ParameterizedType) {
return ((ParameterizedType) containerType).getActualTypeArguments()[0];
}
return Object.class;
}
}
(I filed an issue about that in GitHub)
You use it in the following way:
Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(new StreamTypeAdapterFactory())
.create();
System.out.println(gson.toJson(Stream.of(1, 2, 3)));
I need to replace the DateTime serialization for JSON in WCF REST Self Hosted service. Right now, I'm using something like the following code to do it, but it's definitely not the way to go since it requires manipulating each class.
[DataContract]
public class Test
{
[IgnoreDataMember]
public DateTime StartDate;
[DataMember(Name = "StartDate")]
public string StartDateStr
{
get { return DateUtil.DateToStr(StartDate); }
set { StartDate = DateTime.Parse(value); }
}
}
where my utility function DateUtil.DateToStr does all the formatting work.
Is there any easy way to do it without having to touch the attributes on my classes which have the DataContract attribute? Ideally, there would be no attributes, but a couple of lines of code in my configuration to replace the serializer with one where I've overridden DateTime serialization.
Everything that I've found looks like I have to replace huge pieces of the pipeline.
This article doesn't appear to apply because in I'm using WebServiceHost not HttpServiceHost, which not part of the 4.5.1 Framework.
JSON.NET Serializer for WCF REST Services
By default WCF uses DataContractJsonSerializer to serialize data into JSON. Unfortunatelly date from this serializer is in very difficult format to parse by human brain.
"DateTime": "\/Date(1535481994306+0200)\/"
To override this behavior we need to write custom IDispatchMessageFormatter. This class will receive all data which should be returned to requester and change it according to our needs.
To make it happen to the operations in the endpoint add custom formatter - ClientJsonDateFormatter:
ServiceHost host=new ServiceHost(typeof(CustomService));
host.AddServiceEndpoint(typeof(ICustomContract), new WebHttpBinding(), Consts.WebHttpAddress);
foreach (var endpoint in host.Description.Endpoints)
{
if (endpoint.Address.Uri.Scheme.StartsWith("http"))
{
foreach (var operation in endpoint.Contract.Operations)
{
operation.OperationBehaviors.Add(new ClientJsonDateFormatter());
}
endpoint.Behaviors.Add(new WebHttpBehavior());
}
}
ClientJsonDateFormatter is simple class which just applies formatter ClientJsonDateFormatter
public class ClientJsonDateFormatter : IOperationBehavior
{
public void AddBindingParameters(OperationDescription operationDescription, BindingParameterCollection bindingParameters) { }
public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation) { }
public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation)
{
dispatchOperation.Formatter = new ResponseJsonFormatter(operationDescription);
}
public void Validate(OperationDescription operationDescription) { }
}
In the formatter we took imput and serialize it with the changed Serializer:
public class ResponseJsonFormatter : IDispatchMessageFormatter
{
OperationDescription Operation;
public ResponseJsonFormatter(OperationDescription operation)
{
this.Operation = operation;
}
public void DeserializeRequest(Message message, object[] parameters)
{
}
public Message SerializeReply(MessageVersion messageVersion, object[] parameters, object result)
{
string json=Newtonsoft.Json.JsonConvert.SerializeObject(result);
byte[] bytes = Encoding.UTF8.GetBytes(json);
Message replyMessage = Message.CreateMessage(messageVersion, Operation.Messages[1].Action, new RawDataWriter(bytes));
replyMessage.Properties.Add(WebBodyFormatMessageProperty.Name, new WebBodyFormatMessageProperty(WebContentFormat.Raw));
return replyMessage;
}
}
And to send information to client we need data writer - RawDataWriter. Its implementation is simple:
class RawDataWriter : BodyWriter
{
byte[] data;
public RawDataWriter(byte[] data)
: base(true)
{
this.data = data;
}
protected override void OnWriteBodyContents(XmlDictionaryWriter writer)
{
writer.WriteStartElement("Binary");
writer.WriteBase64(data, 0, data.Length);
writer.WriteEndElement();
}
}
Applying all code will result in returning date in more friendly format:
"DateTime":"2018-08-28T20:56:48.6411976+02:00"
To show it in practice I created example in the github branch DateTimeFormatter.
Please check also this answer as very likely you also will need it.
There is a limitation in JSON to convert DateTime, specially according to your case.
Please see http://msdn.microsoft.com/en-us/library/bb412170(v=vs.110).aspx
and read the section Dates/Times and JSON
To resolve this problem, I simply changed the type of serialization from JSON to XML for all the calls including DateTime.
After long time discussion ,I have find out the solution for it.
Please Use the following Code to Solve serialized date..
[IgnoreDataMember]
public DateTime? PerformanceDate { get; set; }
[DataMember(EmitDefaultValue = false, Name = "PerformanceDate")]
public string UpdateStartDateStr
{
get
{
if (this.PerformanceDate.HasValue)
return this.PerformanceDate.Value.ToUniversalTime().ToString("s", CultureInfo.InvariantCulture);
else
return null;
}
set
{
// should implement this...
}
}
I have the following code.
public class SomeClass {
private List<SomeOtherClass> referrals;
public List<SomeOtherClass> getReferrals() {
return referrals;
}
public void setReferrals( List<SomeOtherClass> referrals) {
this.referrals = referrals;
}
}
I have a json that I read from the wire. It is correctly formatted. I use GSON
My question is :
when I do fromJson(jsonString,SomeClass.class);
it gives an exception.
If I don't use List<SomeOtherClass> but instead use List<String> for referrals.
(in other word a primitive)
And iterate over each String and create SomeOtherClass object it works fine.
Why can't I just use fromJson(jsonString,SomeClass.class);