Ran into a similar problem like the following forum post:
http://jersey.576304.n2.nabble.com/parsing-JSON-with-Arrays-using-Jettison-td5732207.html
Using Resteasy 2.0.1GA with Jettison 1.2 and getting a problem marshalling arrays when involving namespace mappings. See code below. Basically if the number of array entries are greater than one and namespace mappings are used. Anybody else run into this problem? The Nabble form poster got around it by writing a custom unmarshaller.
I either need to isolate the Jettison bug or write a Resteasy extension of the JettisonMappedUnmarshaller class (which hands over the namespace mappings and unmarshaller to the Jettison Configuration).
The following code doesn't unmarshall (post step) if the properties variables contains 2 or more entries.
public class Experimenting {
#Path("test")
public static class MyResource {
#XmlAccessorType(XmlAccessType.FIELD)
#XmlType(name = "Property", propOrder = { "name", "value" })
public static class MyProperty {
#XmlElement(name = "Name", required = true)
protected String name;
#XmlElement(name = "Value", required = true)
protected String value;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
#XmlType(name = "MyElement", propOrder = { "myProperty" })
#XmlAccessorType(XmlAccessType.FIELD)
#XmlRootElement(name = "MyElement", namespace = "http://www.klistret.com/cmdb/ci/commons")
#Mapped(namespaceMap = { #XmlNsMap(namespace = "http://www.klistret.com/cmdb/ci/commons", jsonName = "com.klistret.cmdb.ci.commons") })
public static class MyElement {
#XmlElement(name = "MyProperty", namespace = "http://www.klistret.com/cmdb/ci/commons")
protected List myProperty;
public List getMyProperty() {
if (myProperty == null) {
myProperty = new ArrayList();
}
return this.myProperty;
}
public void setMyProperty(List myProperty) {
this.myProperty = myProperty;
}
}
#GET
#Path("myElement/{id}")
#Produces(MediaType.APPLICATION_JSON)
public MyElement getMy(#PathParam("id")
Long id) {
MyElement myElement = new MyElement();
MyProperty example = new MyProperty();
example.setName("example");
example.setValue("of a property");
MyProperty another = new MyProperty();
another.setName("another");
another.setValue("just a test");
MyProperty[] properties = new MyProperty[] { example, another };
myElement.setMyProperty(Arrays.asList(properties));
return myElement;
}
#POST
#Path("/myElement")
#Consumes(MediaType.APPLICATION_JSON)
#Produces(MediaType.APPLICATION_JSON)
public MyElement createMy(MyElement myElement) {
List properties = myElement.getMyProperty();
System.out.println("Properties size: " + properties.size());
return myElement;
}
}
private Dispatcher dispatcher;
#Before
public void setUp() throws Exception {
// embedded server
dispatcher = MockDispatcherFactory.createDispatcher();
dispatcher.getRegistry().addPerRequestResource(MyResource.class);
}
#Test
public void getAndCreate() throws URISyntaxException,
UnsupportedEncodingException {
MockHttpRequest getRequest = MockHttpRequest.get("/test/element/44");
MockHttpResponse getResponse = new MockHttpResponse();
dispatcher.invoke(getRequest, getResponse);
String getResponseBodyAsString = getResponse.getContentAsString();
System.out.println(String.format(
"Get Response code [%s] with payload [%s]", getResponse
.getStatus(), getResponse.getContentAsString()));
MockHttpRequest postRequest = MockHttpRequest.post("/test/element");
MockHttpResponse postResponse = new MockHttpResponse();
postRequest.contentType(MediaType.APPLICATION_JSON);
postRequest.content(getResponseBodyAsString.getBytes("UTF-8"));
dispatcher.invoke(postRequest, postResponse);
System.out.println(String.format(
"Post Response code [%s] with payload [%s]", postResponse
.getStatus(), postResponse.getContentAsString()));
}
}
Do you have to use Jettison? If not I would recommend just switching to use Jackson instead; this typically solves array/list related problems (problem with Jettison is that it converts to XML model, which makes it very hard to tell arrays from objects -- there are bugs, too, but it is fundamentally hard thing to get working correctly).
Related
How to parse answerData key from json response in kotlin as it is changing its type in each block? I tried keeping it Any but was not able to type cast. how to parse answerData?
{
"status": "OK",
"data": [
{
"id": 10,
"answerData": null
},
{
"id": 21,
"answerData": {
"selectionOptionId": 0,
"selectionOptionText": null
}
},
{
"id": 45,
"answerData": {
"IsAffiliatedWithSeller": false,
"AffiliationDescription": null
}
},
{
"id" : 131,
"answerData" : [
{ "2" : "Chapter 11" },
{ "3" : "Chapter 12" },
{ "1" : "Chapter 7" }
]
},
{
"id" : 140,
"answerData" : [
{
"liabilityTypeId" : 2,
"monthlyPayment" : 200,
"remainingMonth" : 2,
"liabilityName" : "Separate Maintenance",
"name" : "Two"
},
{
"liabilityTypeId" : 1,
"monthlyPayment" : 300,
"remainingMonth" : 1,
"liabilityName" : "Child Support",
"name" : "Three"
}
]
}
]
}
As commented and explained in other answers you really should ask changes to the JSON format. However it is not so unusual to have list of elements of which the data included varies. For such case there should at least be some field indication the type of data to be deserialized. (not saying it is not an anti-pattern sometimes it might be).
If you reach that agreement it is possible to use - for example - RuntimeTypeAdapterFactory
like explained in linked question (sorry it is Java).
If not you will run into troubles. It is still quite easy to isolate the problem. Not saying it is easy to solve. I present one possible (sorry again, Java but guess it is easily adaptable to Kotlin) solution. I have used lots of inner static classes to make the code more compact. The actual logic has not so many rows most of the code is to map your JSON into java classes.
Make the model abstract in a way that it does not hinder Gson to do its job whatever it heads in that problematic field:
#Getter #Setter
public class Response {
private String status;
#Getter #Setter
public static class DataItem {
private Long id;
// below 2 rows explained later, this is what changes
#JsonAdapter(AnswerDataDeserializer.class)
private AnswerData answerData;
}
private DataItem[] data;
}
As you see there is declared this AnswerData and #JsonAdapter for handling the actual more complex stuff:
public class AnswerDataDeserializer
implements JsonDeserializer<AnswerDataDeserializer.AnswerData> {
private final Gson gson = new Gson();
// The trick that makes the field more abstract. No necessarily
// needed answerData might possibly be just Object
public interface AnswerData {
// just to have something here not important
default String getType() {
return getClass().getName();
}
}
// here I have assumed Map<K,V> because of field name cannot be plain number.
#SuppressWarnings("serial")
public static class ChapterDataAnswer extends ArrayList<Map<Long, String>>
implements AnswerData {
}
#SuppressWarnings("serial")
public static class LiabilityDataAnswer
extends ArrayList<LiabilityDataAnswer.LiabilityData>
implements AnswerData {
#Getter #Setter
public static class LiabilityData {
private Long liabilityTypeId;
private Double monthlyPayment;
private Integer remainingMonth;
private String liabilityName;
private String name;
}
}
#Override
public AnswerData deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context)
throws JsonParseException {
if(json.isJsonArray()) {
try {
return gson.fromJson(json, ChapterDataAnswer.class);
} catch (Exception e) {
return gson.fromJson(json, LiabilityDataAnswer.class);
}
}
if(json.isJsonObject()) {
// do something else
}
return null;
}
}
I have above presented only the two more complex array types. But as you can see you will have to check/peek all the deserialized AnswerData in some way to determine the actual type in method deserialize
Now you need still need to know about different types of AnswerData. Maybe there are such types that collide in a way that you cannot determine the type.
NOTE: you can also always also deserialize whole stuff or any object as a Map or Object (Gson will make it LinkedHashMap if I remember correct)
Whether way you do it you still need to check the instance of the object after deserialization what it is and use cast.
The design of the input JSON is terrible and really hard to use.
Let me say that:
it mixes elements and collections for the answerData attributes with dozens of cons against it;
answer elements lack the type discriminator field so the deserialize must analyze each JSON tree to produce a valid deserialized object with another dozen of cons against it (including "there is no way to determine the exact type precisely" and "it may require too much memory because of JSON trees");
Some tools like OpenAPI/Swagger use the discriminator field to deserialize to a dedicated type without doing any heuristics.
Any won't work for you of course, as Gson has no even a single idea what those payloads are supposed to be deserialized to.
Since you didn't provide your mappings, I'll provide mine demonstrating an example idea of how such terrible JSON documents can be deserialized.
This also includes:
using Java 11 and Lombok instead of Kotlin (as it does not really matter as you stated in the notice);
mapping an answer with a list of answers even if the incoming JSON node contains an object instead of an array to unify all of that;
creating a deducing deserializer that naively does some "magic" to get rid of the bad JSON design.
To resolve the first issue, elements vs arrays/lists, I've found a ready-to-use solution right here at S.O.:
#RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public final class AlwaysListTypeAdapterFactory<E> implements TypeAdapterFactory {
#Nullable
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
if (!List.class.isAssignableFrom(typeToken.getRawType())) {
return null;
}
final Type elementType = resolveTypeArgument(typeToken.getType());
#SuppressWarnings("unchecked")
final TypeAdapter<E> elementTypeAdapter = (TypeAdapter<E>) gson.getAdapter(TypeToken.get(elementType));
#SuppressWarnings("unchecked")
final TypeAdapter<T> alwaysListTypeAdapter = (TypeAdapter<T>) new AlwaysListTypeAdapter<>(elementTypeAdapter).nullSafe();
return alwaysListTypeAdapter;
}
private static Type resolveTypeArgument(final Type type) {
if (!(type instanceof ParameterizedType)) {
return Object.class;
}
final ParameterizedType parameterizedType = (ParameterizedType) type;
return parameterizedType.getActualTypeArguments()[0];
}
private static final class AlwaysListTypeAdapter<E> extends TypeAdapter<List<E>> {
private final TypeAdapter<E> elementTypeAdapter;
private AlwaysListTypeAdapter(final TypeAdapter<E> elementTypeAdapter) {
this.elementTypeAdapter = elementTypeAdapter;
}
#Override
public void write(final JsonWriter out, final List<E> list) {
throw new UnsupportedOperationException();
}
#Override
public List<E> read(final JsonReader in) throws IOException {
final List<E> list = new ArrayList<>();
final JsonToken token = in.peek();
switch ( token ) {
case BEGIN_ARRAY:
in.beginArray();
while ( in.hasNext() ) {
list.add(elementTypeAdapter.read(in));
}
in.endArray();
break;
case BEGIN_OBJECT:
case STRING:
case NUMBER:
case BOOLEAN:
list.add(elementTypeAdapter.read(in));
break;
case NULL:
throw new AssertionError("Must never happen: check if the type adapter configured with .nullSafe()");
case NAME:
case END_ARRAY:
case END_OBJECT:
case END_DOCUMENT:
throw new MalformedJsonException("Unexpected token: " + token);
default:
throw new AssertionError("Must never happen: " + token);
}
return list;
}
}
}
Next, for the item no. 2, a deducing type adapter factory might be implemented like this:
#RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public final class DeducingTypeAdapterFactory<V> implements TypeAdapterFactory {
public interface TypeAdapterProvider {
#Nonnull
<T> TypeAdapter<T> provide(#Nonnull TypeToken<T> typeToken);
}
private final Predicate<? super TypeToken<?>> isSupported;
private final BiFunction<? super JsonElement, ? super TypeAdapterProvider, ? extends V> deduce;
public static <V> TypeAdapterFactory create(final Predicate<? super TypeToken<?>> isSupported,
final BiFunction<? super JsonElement, ? super TypeAdapterProvider, ? extends V> deduce) {
return new DeducingTypeAdapterFactory<>(isSupported, deduce);
}
#Override
#Nullable
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
if (!isSupported.test(typeToken)) {
return null;
}
final Map<TypeToken<?>, TypeAdapter<?>> cache = new ConcurrentHashMap<>();
final TypeAdapter<V> deducedTypeAdapter = new TypeAdapter<V>() {
#Override
public void write(final JsonWriter jsonWriter, final V value) {
throw new UnsupportedOperationException();
}
#Override
public V read(final JsonReader jsonReader) {
final JsonElement jsonElement = Streams.parse(jsonReader);
return deduce.apply(jsonElement, new TypeAdapterProvider() {
#Nonnull
#Override
public <TT> TypeAdapter<TT> provide(#Nonnull final TypeToken<TT> typeToken) {
final TypeAdapter<?> cachedTypeAdapter = cache.computeIfAbsent(typeToken, tt -> gson.getDelegateAdapter(DeducingTypeAdapterFactory.this, tt));
#SuppressWarnings("unchecked")
final TypeAdapter<TT> typeAdapter = (TypeAdapter<TT>) cachedTypeAdapter;
return typeAdapter;
}
});
}
}
.nullSafe();
#SuppressWarnings("unchecked")
final TypeAdapter<T> typeAdapter = (TypeAdapter<T>) deducedTypeAdapter;
return typeAdapter;
}
}
Basically, it does no deducing itself, and only delegates the filter and deducing jobs elsewhere using the Strategy design pattern.
Now let's assume your mappings are "general" enough (including using #JsonAdapter for Answer to coerce single elements to become lists):
#RequiredArgsConstructor(access = AccessLevel.PACKAGE, staticName = "of")
#Getter
#EqualsAndHashCode
#ToString
final class Response<T> {
#Nullable
#SerializedName("status")
private final String status;
#Nullable
#SerializedName("data")
private final T data;
}
#RequiredArgsConstructor(access = AccessLevel.PACKAGE, staticName = "of")
#Getter
#EqualsAndHashCode
#ToString
final class Answer {
#SerializedName("id")
private final int id;
#Nullable
#SerializedName("answerData")
#JsonAdapter(AlwaysListTypeAdapterFactory.class)
private final List<AnswerDatum> answerData;
}
#RequiredArgsConstructor(access = AccessLevel.PRIVATE)
abstract class AnswerDatum {
interface Visitor<R> {
R visit(#Nonnull Type1 answerDatum);
R visit(#Nonnull Type2 answerDatum);
R visit(#Nonnull Type3 answerDatum);
R visit(#Nonnull Type4 answerDatum);
}
abstract <R> R accept(#Nonnull Visitor<? extends R> visitor);
#RequiredArgsConstructor(access = AccessLevel.PACKAGE, staticName = "of")
#Getter
#EqualsAndHashCode(callSuper = false)
#ToString(callSuper = false)
static final class Type1 extends AnswerDatum {
#SerializedName("selectionOptionId")
private final int selectionOptionId;
#Nullable
#SerializedName("selectionOptionText")
private final String selectionOptionText;
#Override
<R> R accept(#Nonnull final Visitor<? extends R> visitor) {
return visitor.visit(this);
}
}
#RequiredArgsConstructor(access = AccessLevel.PACKAGE, staticName = "of")
#Getter
#EqualsAndHashCode(callSuper = false)
#ToString(callSuper = false)
static final class Type2 extends AnswerDatum {
#SerializedName("IsAffiliatedWithSeller")
private final boolean isAffiliatedWithSeller;
#Nullable
#SerializedName("AffiliationDescription")
private final String affiliationDescription;
#Override
<R> R accept(#Nonnull final Visitor<? extends R> visitor) {
return visitor.visit(this);
}
}
#RequiredArgsConstructor(access = AccessLevel.PACKAGE, staticName = "of")
#Getter
#EqualsAndHashCode(callSuper = false)
#ToString(callSuper = false)
static final class Type3 extends AnswerDatum {
#Nonnull
private final String key;
#Nullable
private final String value;
#Override
<R> R accept(#Nonnull final Visitor<? extends R> visitor) {
return visitor.visit(this);
}
}
#RequiredArgsConstructor(access = AccessLevel.PACKAGE, staticName = "of")
#Getter
#EqualsAndHashCode(callSuper = false)
#ToString(callSuper = false)
static final class Type4 extends AnswerDatum {
#SerializedName("liabilityTypeId")
private final int liabilityTypeId;
#SerializedName("monthlyPayment")
private final int monthlyPayment;
#SerializedName("remainingMonth")
private final int remainingMonth;
#Nullable
#SerializedName("liabilityName")
private final String liabilityName;
#Nullable
#SerializedName("name")
private final String name;
#Override
<R> R accept(#Nonnull final Visitor<? extends R> visitor) {
return visitor.visit(this);
}
}
}
Note how AnswerDatum uses the Visitor design pattern to avoid explicit type casting.
I'm not sure how it is leveraged in Java when using sealed classes.
public final class DeducingTypeAdapterFactoryTest {
private static final Pattern digitsPattern = Pattern.compile("^\\d+$");
private static final TypeToken<String> stringTypeToken = new TypeToken<>() {};
private static final TypeToken<AnswerDatum.Type1> answerDatumType1TypeToken = new TypeToken<>() {};
private static final TypeToken<AnswerDatum.Type2> answerDatumType2TypeToken = new TypeToken<>() {};
private static final TypeToken<AnswerDatum.Type4> answerDatumType4TypeToken = new TypeToken<>() {};
private static final Gson gson = new GsonBuilder()
.disableInnerClassSerialization()
.disableHtmlEscaping()
.registerTypeAdapterFactory(DeducingTypeAdapterFactory.create(
typeToken -> AnswerDatum.class.isAssignableFrom(typeToken.getRawType()),
(jsonElement, getTypeAdapter) -> {
if ( jsonElement.isJsonObject() ) {
final JsonObject jsonObject = jsonElement.getAsJsonObject();
// type-1? hopefully...
if ( jsonObject.has("selectionOptionId") ) {
return getTypeAdapter.provide(answerDatumType1TypeToken)
.fromJsonTree(jsonElement);
}
// type-2? hopefully...
if ( jsonObject.has("IsAffiliatedWithSeller") ) {
return getTypeAdapter.provide(answerDatumType2TypeToken)
.fromJsonTree(jsonElement);
}
// type-3? hopefully...
if ( jsonObject.size() == 1 ) {
final Map.Entry<String, JsonElement> onlyEntry = jsonObject.entrySet().iterator().next();
final String key = onlyEntry.getKey();
if ( digitsPattern.matcher(key).matches() ) {
final String value = getTypeAdapter.provide(stringTypeToken)
.fromJsonTree(onlyEntry.getValue());
return AnswerDatum.Type3.of(key, value);
}
}
// type-4? hopefully...
if ( jsonObject.has("liabilityTypeId") ) {
return getTypeAdapter.provide(answerDatumType4TypeToken)
.fromJsonTree(jsonElement);
}
}
throw new UnsupportedOperationException("can't parse: " + jsonElement);
}
))
.create();
private static final TypeToken<Response<List<Answer>>> listOfAnswerResponseType = new TypeToken<>() {};
#Test
public void testEqualsAndHashCode() throws IOException {
final Object expected = Response.of(
"OK",
List.of(
Answer.of(
10,
null
),
Answer.of(
21,
List.of(
AnswerDatum.Type1.of(0, null)
)
),
Answer.of(
45,
List.of(
AnswerDatum.Type2.of(false, null)
)
),
Answer.of(
131,
List.of(
AnswerDatum.Type3.of("2", "Chapter 11"),
AnswerDatum.Type3.of("3", "Chapter 12"),
AnswerDatum.Type3.of("1", "Chapter 7")
)
),
Answer.of(
140,
List.of(
AnswerDatum.Type4.of(2, 200, 2, "Separate Maintenance", "Two"),
AnswerDatum.Type4.of(1, 300, 1, "Child Support", "Three")
)
)
)
);
try (final JsonReader jsonReader = openJsonInput()) {
final Object actual = gson.fromJson(jsonReader, listOfAnswerResponseType.getType());
Assertions.assertEquals(expected, actual);
}
}
#Test
public void testVisitor() throws IOException {
final Object expected = List.of(
"21:0",
"45:false",
"131:2:Chapter 11",
"131:3:Chapter 12",
"131:1:Chapter 7",
"140:Two",
"140:Three"
);
try (final JsonReader jsonReader = openJsonInput()) {
final Response<List<Answer>> response = gson.fromJson(jsonReader, listOfAnswerResponseType.getType());
final List<Answer> data = response.getData();
assert data != null;
final Object actual = data.stream()
.flatMap(answer -> Optional.ofNullable(answer.getAnswerData())
.map(answerData -> answerData.stream()
.map(answerDatum -> answerDatum.accept(new AnswerDatum.Visitor<String>() {
#Override
public String visit(#Nonnull final AnswerDatum.Type1 answerDatum) {
return answer.getId() + ":" + answerDatum.getSelectionOptionId();
}
#Override
public String visit(#Nonnull final AnswerDatum.Type2 answerDatum) {
return answer.getId() + ":" + answerDatum.isAffiliatedWithSeller();
}
#Override
public String visit(#Nonnull final AnswerDatum.Type3 answerDatum) {
return answer.getId() + ":" + answerDatum.getKey() + ':' + answerDatum.getValue();
}
#Override
public String visit(#Nonnull final AnswerDatum.Type4 answerDatum) {
return answer.getId() + ":" + answerDatum.getName();
}
})
)
)
.orElse(Stream.empty())
)
.collect(Collectors.toUnmodifiableList());
Assertions.assertEquals(expected, actual);
}
}
private static JsonReader openJsonInput() throws IOException {
return // ... your code code here ...
}
}
That's it.
I find it pretty difficult and unnecessarily complicated.
Please ask your server-side mates to fix their design for good (note how the current situation makes deserializing harder than it might be when designed well).
The Json response is wrong. There is no need to handle this response in client side, the Json response should be changed from the server side. Otherwise this is going to be a huge burden for you in future. A Json object should have a properly defined keys and its values.
I am trying to create a custom Json converter that has no default constructor and instead takes a factory that is dependency injected by Autofac. When ever I hit the object that uses this converter I get an exception that there is no no-arg constructor to use for the deserialization.
I have an objects and primitives. One of the objects is an abstract base object that I have the converter on. Since this converter is abstract I want to dependency inject a factory into the converter's ReadJson method to make the choice as to what conversion to make.
Currently the code is something like the following:
using System;
using System.Collections.Generic;
using Autofac;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using Newtonsoft = Newtonsoft.Json;
public class JsonModule : Module
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<SubThingFactory>()
.As<IFactory>()
.SingleInstance();
builder.Register(c => this.CreateJsonSerializerSettings(c)).SingleInstance();
builder.RegisterType<CamelCasePropertyNamesContractResolver>()
.As<IContractResolver>()
.SingleInstance();
builder.RegisterType<IsoDateTimeConverter>()
.As<Newtonsoft.JsonConverter>()
.SingleInstance();
builder.RegisterType<SubThingConverter>()
.As<Newtonsoft.JsonConverter>()
.SingleInstance();
builder.Register(c => new StringEnumConverter
{
CamelCaseText = true
})
.As<Newtonsoft.JsonConverter>()
.SingleInstance();
}
private Newtonsoft.JsonSerializerSettings CreateJsonSerializerSettings(IComponentContext context)
{
var settings = new Newtonsoft.JsonSerializerSettings
{
DefaultValueHandling = Newtonsoft.DefaultValueHandling.Ignore,
NullValueHandling = Newtonsoft.NullValueHandling.Ignore,
DateTimeZoneHandling = Newtonsoft.DateTimeZoneHandling.Utc
};
settings.ContractResolver = context.Resolve<IContractResolver>();
foreach (var converter in context.Resolve<IEnumerable<Newtonsoft.JsonConverter>>())
{
settings.Converters.Add(converter);
}
return settings;
}
}
public class ThingBeingDeserialized
{
private string Name;
private SubThing subby;
}
[Newtonsoft.JsonConverterAttribute(typeof(SubThingConverter))]
public abstract class SubThing
{
public string Name { get; set; }
public virtual string GetName()
{
//Uses reflection to get the name from a custom attribute
return this.Name;
}
}
[CustomName("A")]
public class SubThingA : SubThing
{
public int Field1 { get; set; }
}
[CustomName("B")]
public class SubThingB : SubThing
{
public string Field2 { get; set; }
}
public class SubThingConverter : Newtonsoft.JsonConverter
{
//This is Autofac injected in
private readonly IFactory factory;
public SubThingConverter(IFactory factory)
{
this.factory = factory;
}
public override object ReadJson(Newtonsoft.JsonReader reader, Type objectType, object existingValue, Newtonsoft.JsonSerializer serializer)
{
if (reader.TokenType == Newtonsoft.JsonToken.Null)
{
return null;
}
var jsonObject = JObject.Load(reader);
var type = jsonObject["type"].ToString();
return this.factory.GetSubThing(type, jsonObject);
}
public override void WriteJson(Newtonsoft.JsonWriter writer, object value, Newtonsoft.JsonSerializer serializer)
{
var type = value.GetType();
var properties = type.GetProperties();
var jObject = new JObject
{
{ "type", type.Name }
};
foreach (var prop in properties)
{
if (prop.CanRead)
{
var propVal = prop.GetValue(value, null);
if (propVal != null)
{
jObject.Add(prop.Name, JToken.FromObject(propVal, serializer));
}
}
}
jObject.WriteTo(writer);
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(SubThing);
}
}
public interface IFactory
{
SubThing GetSubThing(string type, JObject restOfObj);
}
public class SubThingFactory : IFactory
{
public SubThing GetSubThing(string type, JObject restOfObj)
{
switch (type)
{
case "A":
return new SubThingA
{
Field1 = (int)(restOfObj["Field1"])
};
case "B":
return new SubThingB
{
Field2 = (string)(restOfObj["Field2"])
};
}
return null;
}
}
public class CustomNameAttribute : Attribute
{
public CustomNameAttribute(string name)
{
this.Name = name;
}
public string Name { get; set; }
}
The way I am doing the Autofac injection for the JsonSerializerSettings is by registering the settings such that the settings.Converters will pick up the enumeration of all the JsonConverters that are registered with the Autofac container and the SubThingConverter is registered such that when it is resolved it will have the IFactory resolved for it and the JsonSerializer also comes from the autofac container with these settings.
Even when I skip the dependency injection and use a new JsonSerializer with the JsonSerializerSettings with the custom converter added as
settings.Converters.Add(new SubThingConverter(new SubThingFactory()))
I still get the complaint that the SubThingConverter does not have a no arg constructor.
It seems to me that overridding the settings to explicitly use this converter should be enough. I also tried adding in the object[] params in the JsonConverter attribute on the SubThing, I couldn't get it to work and it seems to need to be a compile time array, which doesn't work with the dependency injection I need to do. Any pointers would be greatly appreciated. Thank you!
I am writing a rest service using spring MVC which produces JSON response. It should allow client to select only the given fields in response, means client can mention the fields he is interested in as url parameter like ?fields=field1,field2.
Using Jackson annotations does not provide what I am looking for as it is not dynamic also the filters in Jackson doesnt seem to be promising enough.
So far I am thinking to implement a custom message converter which can take care of this.
Is there any other better way to achieve this? I would like if this logic is not coupled with my services or controllers.
From Spring 4.2, #JsonFilter is supported in MappingJacksonValue
Issue : SPR-12586 : Support Jackson #JsonFilter
Commit : ca06582
You can directly inject PropertyFilter to MappingJacksonValue in a controller.
#RestController
public class BookController {
private static final String INCLUSION_FILTER = "inclusion";
#RequestMapping("/novels")
public MappingJacksonValue novel(String[] include) {
#JsonFilter(INCLUSION_FILTER)
class Novel extends Book {}
Novel novel = new Novel();
novel.setId(3);
novel.setTitle("Last summer");
novel.setAuthor("M.K");
MappingJacksonValue res = new MappingJacksonValue(novel);
PropertyFilter filter = SimpleBeanPropertyFilter.filterOutAllExcept(include);
FilterProvider provider = new SimpleFilterProvider().addFilter(INCLUSION_FILTER, filter);
res.setFilters(provider);
return res;
}
or you can declare global policy by ResponseBodyAdvice. The following example implements filtering policy by "exclude" parameter.
#ControllerAdvice
public class DynamicJsonResponseAdvice extends AbstractMappingJacksonResponseBodyAdvice {
public static final String EXCLUDE_FILTER_ID = "dynamicExclude";
private static final String WEB_PARAM_NAME = "exclude";
private static final String DELI = ",";
private static final String[] EMPTY = new String[]{};
#Override
protected void beforeBodyWriteInternal(MappingJacksonValue container, MediaType contentType,
MethodParameter returnType, ServerHttpRequest req, ServerHttpResponse res) {
if (container.getFilters() != null ) {
// It will be better to merge FilterProvider
// If 'SimpleFilterProvider.addAll(FilterProvider)' is provided in Jackson, it will be easier.
// But it isn't supported yet.
return;
}
HttpServletRequest baseReq = ((ServletServerHttpRequest) req).getServletRequest();
String exclusion = baseReq.getParameter(WEB_PARAM_NAME);
String[] attrs = StringUtils.split(exclusion, DELI);
container.setFilters(configFilters(attrs));
}
private FilterProvider configFilters(String[] attrs) {
String[] ignored = (attrs == null) ? EMPTY : attrs;
PropertyFilter filter = SimpleBeanPropertyFilter.serializeAllExcept(ignored);
return new SimpleFilterProvider().addFilter(EXCLUDE_FILTER_ID, filter);
}
}
IMHO, the simplest way to do that would be to use introspection to dynamically generate a hash containing selected fields and then serialize that hash using Json. You simply have to decide what is the list of usable fields (see below).
Here are two example functions able to do that, first gets all public fields and public getters, the second gets all declared fields (including private ones) in current class and all its parent classes :
public Map<String, Object> getPublicMap(Object obj, List<String> names)
throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
List<String> gettedFields = new ArrayList<String>();
Map<String, Object> values = new HashMap<String, Object>();
for (Method getter: obj.getClass().getMethods()) {
if (getter.getName().startsWith("get") && (getter.getName().length > 3)) {
String name0 = getter.getName().substring(3);
String name = name0.substring(0, 1).toLowerCase().concat(name0.substring(1));
gettedFields.add(name);
if ((names == null) || names.isEmpty() || names.contains(name)) {
values.put(name, getter.invoke(obj));
}
}
}
for (Field field: obj.getClass().getFields()) {
String name = field.getName();
if ((! gettedFields.contains(name)) && ((names == null) || names.isEmpty() || names.contains(name))) {
values.put(name, field.get(obj));
}
}
return values;
}
public Map<String, Object> getFieldMap(Object obj, List<String> names)
throws IllegalArgumentException, IllegalAccessException {
Map<String, Object> values = new HashMap<String, Object>();
for (Class<?> clazz = obj.getClass(); clazz != Object.class; clazz = clazz.getSuperclass()) {
for (Field field : clazz.getDeclaredFields()) {
String name = field.getName();
if ((names == null) || names.isEmpty() || names.contains(name)) {
field.setAccessible(true);
values.put(name, field.get(obj));
}
}
}
return values;
}
Then you only have to get the result of one of this function (or of one you could adapt to your requirements) and serialize it with Jackson.
If you have custom encoding of you domain objects, you would have to maintain the serialization rules in two different places : hash generation and Jackson serialization. In that case, you could simply generate the full class serialization with Jackson and filter the generated string afterwards. Here is an example of such a filter function :
public String jsonSub(String json, List<String> names) throws IOException {
if ((names == null) || names.isEmpty()) {
return json;
}
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> map = mapper.readValue(json, HashMap.class);
for (String name: map.keySet()) {
if (! names.contains(name)) {
map.remove(name);
}
}
return mapper.writeValueAsString(map);
}
Edit : integration in Spring MVC
As you are speaking of a web service and of Jackson, I assume that you use Spring RestController or ResponseBody annotations and (under the hood) a MappingJackson2HttpMessageConverter. If you use Jackson 1 instead, it should be a MappingJacksonHttpMessageConverter.
What I propose is simply to add a new HttpMessageConverter that could make use of one of the above filtering functions, and delegate actual work (and also ancilliary methods) to a true MappingJackson2HttpMessageConverter. In the write method of that new converter, it is possible to have access to the eventual fields request parameter with no need for an explicit ThreadLocal variable thanks to Spring RequestContextHolder. That way :
you keep a clear separation of roles with no modification on existing controllers
you have no modification in Jackson2 configuration
you need no new ThreadLocal variable and simply use a Spring class in a class already tied to Spring since it implements HttpMessageConverter
Here is an example of such a message converter :
public class JsonConverter implements HttpMessageConverter<Object> {
private static final Logger logger = LoggerFactory.getLogger(JsonConverter.class);
// a real message converter that will respond to ancilliary methods and do the actual work
private HttpMessageConverter<Object> delegate =
new MappingJackson2HttpMessageConverter();
// allow configuration of the fields name
private String fieldsParam = "fields";
public void setFieldsParam(String fieldsParam) {
this.fieldsParam = fieldsParam;
}
#Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return delegate.canRead(clazz, mediaType);
}
#Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return delegate.canWrite(clazz, mediaType);
}
#Override
public List<MediaType> getSupportedMediaTypes() {
return delegate.getSupportedMediaTypes();
}
#Override
public Object read(Class<? extends Object> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return delegate.read(clazz, inputMessage);
}
#Override
public void write(Object t, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
// is there a fields parameter in request
String[] fields = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest().getParameterValues(fieldsParam);
if (fields != null && fields.length != 0) {
// get required field names
List<String> names = new ArrayList<String>();
for (String field : fields) {
String[] f_names = field.split("\\s*,\\s*");
names.addAll(Arrays.asList(f_names));
}
// special management for Map ...
if (t instanceof Map) {
Map<?, ?> tmap = (Map<?, ?>) t;
Map<String, Object> map = new LinkedHashMap<String, Object>();
for (Entry entry : tmap.entrySet()) {
String name = entry.getKey().toString();
if (names.contains(name)) {
map.put(name, entry.getValue());
}
}
t = map;
} else {
try {
Map<String, Object> map = getMap(t, names);
t = map;
} catch (Exception ex) {
throw new HttpMessageNotWritableException("Error in field extraction", ex);
}
}
}
delegate.write(t, contentType, outputMessage);
}
/**
* Create a Map by keeping only some fields of an object
* #param obj the Object
* #param names names of the fields to keep in result Map
* #return a map containing only requires fields and their value
* #throws IllegalArgumentException
* #throws IllegalAccessException
*/
public static Map<String, Object> getMap(Object obj, List<String> names)
throws IllegalArgumentException, IllegalAccessException {
Map<String, Object> values = new HashMap<String, Object>();
for (Class<?> clazz = obj.getClass(); clazz != Object.class; clazz = clazz.getSuperclass()) {
for (Field field : clazz.getDeclaredFields()) {
String name = field.getName();
if (names.contains(name)) {
field.setAccessible(true);
values.put(name, field.get(obj));
}
}
}
return values;
}
}
If you want the converter to be more versatile, you could define an interface
public interface FieldsFilter {
Map<String, Object> getMap(Object obj, List<String> names)
throws IllegalAccessException, IllegalArgumentException, InvocationTargetException;
}
and inject it with an implementation of that.
Now you must ask Spring MVC to use that custom message controller.
If you use XML config, you simply declare it in the <mvc:annotation-driven> element :
<mvc:annotation-driven >
<mvc:message-converters>
<bean id="jsonConverter" class="org.example.JsonConverter"/>
</mvc:message-converters>
</mvc:annotation-driven>
And if you use Java configuration, it is almost as simple :
#EnableWebMvc
#Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
#Autowired JsonConverter jsonConv;
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(jsonConv);
StringHttpMessageConverter stringConverter = new StringHttpMessageConverter();
stringConverter.setWriteAcceptCharset(false);
converters.add(new ByteArrayHttpMessageConverter());
converters.add(stringConverter);
converters.add(new ResourceHttpMessageConverter());
converters.add(new SourceHttpMessageConverter<Source>());
converters.add(new AllEncompassingFormHttpMessageConverter());
converters.add(new MappingJackson2HttpMessageConverter());
}
}
but here you have to explicitely add all the default message converters that you need.
I've never done this but after looking at this page http://wiki.fasterxml.com/JacksonFeatureJsonFilter it seems that it would be possible to do what you want this way:
1) Create a custom JacksonAnnotationIntrospector implementation (by extending default one) that will use a ThreadLocal variable to choose a filter for current request and also create a custom FilterProvider that would provide that filter.
2) Configure the message converter's ObjectMapper to use the custom introspector and filter provider
3) Create an MVC interceptor for REST service that detects fields request parameter and configures a new filter for current request via your custom filter provider (this should be a thread local filter). ObjectMapper should pick it up through your custom JacksonAnnotationIntrospector.
I'm not 100% certain that this solution would be thread safe (it depends on how ObjectMapper uses annotation introspector and filter provider internally).
- EDIT -
Ok I did a test implementation and found out that step 1) wouldn't work because Jackson caches the result of AnnotationInterceptor per class. I modified idea to apply dynamic filtering only on annotated controller methods and only if the object doesn't have anoter JsonFilter already defined.
Here's the solution (it's quite lengthy):
DynamicRequestJsonFilterSupport class manages the per-request fields to be filtered out:
public class DynamicRequestJsonFilterSupport {
public static final String DYNAMIC_FILTER_ID = "___DYNAMIC_FILTER";
private ThreadLocal<Set<String>> filterFields;
private DynamicIntrospector dynamicIntrospector;
private DynamicFilterProvider dynamicFilterProvider;
public DynamicRequestJsonFilterSupport() {
filterFields = new ThreadLocal<Set<String>>();
dynamicFilterProvider = new DynamicFilterProvider(filterFields);
dynamicIntrospector = new DynamicIntrospector();
}
public FilterProvider getFilterProvider() {
return dynamicFilterProvider;
}
public AnnotationIntrospector getAnnotationIntrospector() {
return dynamicIntrospector;
}
public void setFilterFields(Set<String> fieldsToFilter) {
filterFields.set(Collections.unmodifiableSet(new HashSet<String>(fieldsToFilter)));
}
public void setFilterFields(String... fieldsToFilter) {
filterFields.set(Collections.unmodifiableSet(new HashSet<String>(Arrays.asList(fieldsToFilter))));
}
public void clear() {
filterFields.remove();
}
public static class DynamicIntrospector extends JacksonAnnotationIntrospector {
#Override
public Object findFilterId(Annotated annotated) {
Object result = super.findFilterId(annotated);
if (result != null) {
return result;
} else {
return DYNAMIC_FILTER_ID;
}
}
}
public static class DynamicFilterProvider extends FilterProvider {
private ThreadLocal<Set<String>> filterFields;
public DynamicFilterProvider(ThreadLocal<Set<String>> filterFields) {
this.filterFields = filterFields;
}
#Override
public BeanPropertyFilter findFilter(Object filterId) {
return null;
}
#Override
public PropertyFilter findPropertyFilter(Object filterId, Object valueToFilter) {
if (filterId.equals(DYNAMIC_FILTER_ID) && filterFields.get() != null) {
return SimpleBeanPropertyFilter.filterOutAllExcept(filterFields.get());
}
return super.findPropertyFilter(filterId, valueToFilter);
}
}
}
JsonFilterInterceptor intercepts controller methods annotated with custom #ResponseFilter annotation.
public class JsonFilterInterceptor implements HandlerInterceptor {
#Autowired
private DynamicRequestJsonFilterSupport filterSupport;
private ThreadLocal<Boolean> requiresReset = new ThreadLocal<Boolean>();
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod method = (HandlerMethod) handler;
ResponseFilter filter = method.getMethodAnnotation(ResponseFilter.class);
String[] value = filter.value();
String param = filter.param();
if (value != null && value.length > 0) {
filterSupport.setFilterFields(value);
requiresReset.set(true);
} else if (param != null && param.length() > 0) {
String filterParamValue = request.getParameter(param);
if (filterParamValue != null) {
filterSupport.setFilterFields(filterParamValue.split(","));
}
}
}
requiresReset.remove();
return true;
}
#Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
#Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
Boolean reset = requiresReset.get();
if (reset != null && reset) {
filterSupport.clear();
}
}
}
Here's the custom #ResponseFilter annotation. You can either define a static filter (via annotation's value property) or a filter based on request param (via annotation's param property):
#Target({ElementType.METHOD, ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface ResponseFilter {
String[] value() default {};
String param() default "";
}
You will need to setup the message converter and the interceptor in the config class:
...
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(converter());
}
#Bean
JsonFilterInterceptor jsonFilterInterceptor() {
return new JsonFilterInterceptor();
}
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jsonFilterInterceptor);
}
#Bean
DynamicRequestJsonFilterSupport filterSupport() {
return new DynamicRequestJsonFilterSupport();
}
#Bean
MappingJackson2HttpMessageConverter converter() {
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
ObjectMapper mapper = new ObjectMapper();
mapper.setAnnotationIntrospector(filterSupport.getAnnotationIntrospector());
mapper.setFilters(filterSupport.getFilterProvider());
converter.setObjectMapper(mapper);
return converter;
}
...
And finally, you can use the filter like this:
#RequestMapping("/{id}")
#ResponseFilter(param = "fields")
public Invoice getInvoice(#PathVariable("id") Long id) { ... }
When request is made to /invoices/1?fields=id,number response will be
filtered and only id and number properties will be returned.
Please note I haven't tested this thoroughly but it should get you started.
Would populating a HashMap from the object not suite the requirements? You could then just parse the HashMap. I have done something similar with GSON in the past where I had to provide a simple entity and ended up just populating a HashMap and then serializing it, it was far more maintainable than over engineering a whole new system.
public class RESTDataServiceClient{
private Client client;
private String dataServiceUri;
private String dataServiceResource;
private CustomData customData;
public RESTDataServiceClient(String dataServiceUri, String dataServiceResource, Client client){
this.client = client;
this.dataServiceUri = dataServiceUri;
this.dataServiceResource = dataServiceResource;
}
#Override
public CustomData getCustomData() {
WebTarget dataServiceTarget = client.target(dataServiceUri).path(dataServiceResource);
Invocation.Builder invocationBuilder = dataServiceTarget.request(MediaType.APPLICATION_JSON_TYPE);
Response response = invocationBuilder.get();
myCustomData = response.readEntity(CustomData.class);
return myCustomData;
}
}
CustomData.java
public class CustomData{
private TLongObjectMap<Map<String, TIntIntMap>> data;
public CustomData() {
this.data = new TLongObjectHashMap<>();
}
//getter and setter
}
sample json content
{"50000":{"testString":{"1":10}},"50001":{"testString1":{"2":11}} }
I am trying to get data from a data service which is going to return data in a JSON format. I am trying to write a client to read that JSON into a custom object. The CustomData contains a nested trove map datastructure. we wrote a custom serializer for that and the server part works fine. I am unable to get the rest client read the data into an object, but reading into string works. I tried above pasted code with the sample data and i get the error below.
javax.ws.rs.ProcessingException: Error reading entity from input stream.
at org.glassfish.jersey.message.internal.InboundMessageContext.readEntity(InboundMessageContext.java:866)
at org.glassfish.jersey.message.internal.InboundMessageContext.readEntity(InboundMessageContext.java:783)
at org.glassfish.jersey.client.ClientResponse.readEntity(ClientResponse.java:326)
at org.glassfish.jersey.client.InboundJaxrsResponse$1.call(InboundJaxrsResponse.java:111)
at org.glassfish.jersey.internal.Errors.process(Errors.java:315)
at org.glassfish.jersey.internal.Errors.process(Errors.java:297)
at org.glassfish.jersey.internal.Errors.process(Errors.java:228)
at org.glassfish.jersey.process.internal.RequestScope.runInScope(RequestScope.java:399)
at org.glassfish.jersey.client.InboundJaxrsResponse.readEntity(InboundJaxrsResponse.java:108)
at com.sample.data.RESTDataServiceClient.getCustomData(RESTDataServiceClient.java:42)
Caused by: com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "50000" (class com.sample.data.CustomData), not marked as ignorable (0 known properties: ])
at [Source: org.glassfish.jersey.message.internal.ReaderInterceptorExecutor$UnCloseableInputStream#2cb89281; line: 1, column: 14] (through reference chain: com.sample.data.CustomData["50000"])
at com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException.from(UnrecognizedPropertyException.java:51)
at com.fasterxml.jackson.databind.DeserializationContext.reportUnknownProperty(DeserializationContext.java:671)
at com.fasterxml.jackson.databind.deser.std.StdDeserializer.handleUnknownProperty(StdDeserializer.java:773)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.handleUnknownProperty(BeanDeserializerBase.java:1297)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.handleUnknownVanilla(BeanDeserializerBase.java:1275)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:247)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:118)
at com.fasterxml.jackson.databind.ObjectReader._bind(ObjectReader.java:1233)
at com.fasterxml.jackson.databind.ObjectReader.readValue(ObjectReader.java:677)
at com.fasterxml.jackson.jaxrs.base.ProviderBase.readFrom(ProviderBase.java:777)
at org.glassfish.jersey.message.internal.ReaderInterceptorExecutor$TerminalReaderInterceptor.invokeReadFrom(ReaderInterceptorExecutor.java:264)
at org.glassfish.jersey.message.internal.ReaderInterceptorExecutor$TerminalReaderInterceptor.aroundReadFrom(ReaderInterceptorExecutor.java:234)
at org.glassfish.jersey.message.internal.ReaderInterceptorExecutor.proceed(ReaderInterceptorExecutor.java:154)
at org.glassfish.jersey.message.internal.MessageBodyFactory.readFrom(MessageBodyFactory.java:1124)
at org.glassfish.jersey.message.internal.InboundMessageContext.readEntity(InboundMessageContext.java:851)
... 38 more
TLongObjectMap is not deserializable out of the box, so how you made a custom serializer you also need to implement a custom deserializer. You can package these up nicely in a module and add it to your ObjectMapper.
It looks like there is a Trove module in development right now, which you can download and add to your ObjectMapper the same as the example below. The TIntObjectMapDeserializer implementation in that link is much more robust then my solution, so I would recommend using that class in your project if possible.
If you want to try and write it yourself, here's a starting point that properly deserializes your provided example:
public class FakeTest {
#Test
public void test() throws Exception {
ObjectMapper om = new ObjectMapper();
om.registerModule(new CustomModule());
String s = "{\"50000\":{\"testString\":{\"1\":10}},\"50001\":{\"testString1\":{\"2\":11}} }";
CustomData cd = om.readValue(s, CustomData.class);
System.out.println(cd.getData());
}
public static class CustomData {
private TLongObjectMap<Map<String, TIntIntMap>> data;
public CustomData() {
this.data = new TLongObjectHashMap<>();
}
public TLongObjectMap<Map<String, TIntIntMap>> getData() { return data; }
public void setData(TLongObjectMap<Map<String, TIntIntMap>> data) { this.data = data; }
}
public static class CustomModule extends SimpleModule {
public CustomModule() {
addSerializer(CustomData.class, new CustomSerializer());
addDeserializer(CustomData.class, new CustomDeserializer());
}
public static class CustomSerializer extends JsonSerializer<CustomData> {
#Override
public void serialize(CustomData value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
// add custom serializer here
}
}
public static class CustomDeserializer extends JsonDeserializer<CustomData> {
#Override
public CustomData deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
TLongObjectMap<Map<String, TIntIntMap>> data = new TLongObjectHashMap<>();
ObjectNode node = jsonParser.getCodec().readTree(jsonParser);
Iterator<Map.Entry<String,JsonNode>> fields = node.fields();
while (fields.hasNext()) {
Map.Entry<String, JsonNode> entry = fields.next();
ObjectNode value = (ObjectNode) entry.getValue();
Map.Entry<String, JsonNode> innerField = value.fields().next();
ObjectNode innerNode = (ObjectNode) innerField.getValue();
Map.Entry<String, JsonNode> innerInnerField = innerNode.fields().next();
TIntIntMap intMap = new TIntIntHashMap();
intMap.put(Integer.parseInt(innerInnerField.getKey()), innerInnerField.getValue().asInt());
Map<String, TIntIntMap> innerMap = Collections.singletonMap(innerField.getKey(), intMap);
data.put(Long.parseLong(entry.getKey()), innerMap);
}
CustomData customData = new CustomData();
customData.setData(data);
return customData;
}
}
}
}
Is it possible to serialize an object using toJson(object) and have the toJson-parser ignore certain methods?
We have a method in a User class (getSocial - which is concerned with Facebook integration) that makes the toJson()-parsing fail - and we'd like it go ignore that method when serializing if possible.
Can this be done?
You can just iterate the object and rewrite it to Map or List with specified values only.
Note that if you are choosing your objects with Ebean it fetches whole object, also data, which shouldn't be fetched (as password or other credentials)
use fastjson & PropertyFilter:
Sample Code
import com.alibaba.fastjson.serializer.JSONSerializer;
import com.alibaba.fastjson.serializer.PropertyFilter;
import com.alibaba.fastjson.serializer.SerializeWriter;
PropertyFilter filter = new PropertyFilter() {
public boolean apply(Object source, String name, Object value) {
return false;
}
};
SerializeWriter out = new SerializeWriter();
JSONSerializer serializer = new JSONSerializer(out);
serializer.getPropertyFilters().add(filter);
A a = new A();
serializer.write(a);
String text = out.toString();
Assert.assertEquals("{}", text);
PropertyFilter
PropertyFilter filter = new PropertyFilter() {
public boolean apply(Object source, String name, Object value) {
if("name".equals(name)) {
return true;
}
return false;
}
};
SerializeWriter out = new SerializeWriter();
JSONSerializer serializer = new JSONSerializer(out);
serializer.getPropertyFilters().add(filter);
A a = new A();
a.setName("chennp2008");
serializer.write(a);
String text = out.toString();
Assert.assertEquals("{\"name\":\"chennp2008\"}", text);
ValueObject:
public static class A {
private int id;
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}