RealmProxyClassGenerator needs to add the annotation indicated from source RealmObject class
Expected Results
If I added an annotation from the source annotation for example:
@JsonObject
public class User extends RealmObject {
The RealmProxy class counterpart should have the annotation inherited as well:
@JsonObject
public class UserRealmProxy extends entities.realmobjects.User
implements RealmObjectProxy, UserRealmProxyInterface
No inheritance of Annotation occurring on the RealmProxy class
Version of Realm and tooling
Realm version(s): 1.1.0
Android Studio version: 2.1.2
Hi @peterbetos I am not fully sure what you mean by the title for this issue. Please fill out the rest of the template as well.
Edited the issue entry. :)
Thanks :)
What is the use case for this?
Annotations are inherited so the proxy classes should count as having the annotation.
This is for RealmObjects instantiated from using Realm "where" method,
example: realm.where(User.class).findFirst();
but needs to be serialized by a parser e.g. LoganSquare.
The object returned from the where operation is usually a Proxy class
Yup, objects returned by where() is always proxy objects. Have you verified it isn't working currently because the annotations should be inherited: http://stackoverflow.com/questions/10596744/is-it-possible-for-class-to-inherit-the-annotaions-of-the-super-class
Perhaps it is LoganSquare that is not inspecting the annotations properly? I am not that familiar with LoganSquare to be able to tell how they do this.
Note, I am not inherently against us adding the annotations to the proxy class, just trying to grasp what problems it would solve and if it should be solved elsewhere instead.
It seems to be an explicit requirement for the LoganSquare parsing.
The following exception is returned if the annotation is not explicitly mentioned:
com.bluelinelabs.logansquare.NoSuchMapperException: Class io.realm.UserRealmProxy could not be mapped to a JSON object. Perhaps it hasn't been annotated with @JsonObject?
How does LoganSquare access the object data? If they use reflection it will not work as our proxy classes do not have the information in the field. So adding the annotation will not accomplish much.
A work-around that would work now is doing a copyFromRealm and then serialize that:
String json = LoganSquare.serialize(realm.copyFromRealm(obj));
Does copyFromRealm produce the RealmObject extended version of the class?
No, it produces an instance of the original class.
@cmelchior LoganSquare uses an annotation processor to generate the JSON-parse jackson core stream code, but it uses .class to determine the type of mapper to be selected for the current given object.
/**
* Returns a JsonMapper for a given class that has been annotated with @JsonObject.
*
* @param cls The class for which the JsonMapper should be fetched.
*/
public static <E> JsonMapper<E> mapperFor(Class<E> cls) throws NoSuchMapperException {
JsonMapper<E> mapper = getMapper(cls);
if (mapper == null) {
throw new NoSuchMapperException(cls);
} else {
return mapper;
}
}
@SuppressWarnings("unchecked")
/*package*/ static <E> JsonMapper<E> getMapper(Class<E> cls) {
JsonMapper<E> mapper = OBJECT_MAPPERS.get(cls);
if (mapper == null) {
// The only way the mapper wouldn't already be loaded into OBJECT_MAPPERS is if it was compiled separately, but let's handle it anyway
try {
Class<?> mapperClass = Class.forName(cls.getName() + Constants.MAPPER_CLASS_SUFFIX);
mapper = (JsonMapper<E>)mapperClass.newInstance();
OBJECT_MAPPERS.put(cls, mapper);
} catch (Exception ignored) { }
}
return mapper;
}
As such, mapperFor(Class<?> clazz) will fail for the realm proxy. It works with copyFromRealm() though
It did work. Thanks :)
But if we're saying that it's not using reflection then I suggest pursuing this issue still :)
Thanks @Zhuinden, although the crux of the problem is what the mapper class is doing. Most JSON libraries uses reflection, which doesn't work on Realm proxy classes. In that case the only work-around is using copyFromRealm() first. Us adding the annotation to the proxy class will not fix it.
@cmelchior I don't think the Mapper class would itself fail. It uses annotation processing to generate it.
This is an example
import com.bluelinelabs.logansquare.JsonMapper;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import java.io.IOException;
import java.lang.Long;
import java.lang.Override;
import java.lang.String;
import java.lang.SuppressWarnings;
import java.util.ArrayList;
import java.util.List;
@SuppressWarnings("unsafe,unchecked")
public final class FeedItemBO$$JsonObjectMapper extends JsonMapper<FeedItemBO> {
@Override
public FeedItemBO parse(JsonParser jsonParser) throws IOException {
FeedItemBO instance = new FeedItemBO();
if (jsonParser.getCurrentToken() == null) {
jsonParser.nextToken();
}
if (jsonParser.getCurrentToken() != JsonToken.START_OBJECT) {
jsonParser.skipChildren();
return null;
}
while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
String fieldName = jsonParser.getCurrentName();
jsonParser.nextToken();
parseField(instance, fieldName, jsonParser);
jsonParser.skipChildren();
}
return instance;
}
@Override
public void parseField(FeedItemBO instance, String fieldName, JsonParser jsonParser) throws IOException {
if ("category_ids".equals(fieldName)) {
if (jsonParser.getCurrentToken() == JsonToken.START_ARRAY) {
ArrayList<Long> collection1 = new ArrayList<Long>();
while (jsonParser.nextToken() != JsonToken.END_ARRAY) {
Long value1;
value1 = jsonParser.getCurrentToken() == JsonToken.VALUE_NULL ? null : Long.valueOf(jsonParser.getValueAsLong());
collection1.add(value1);
}
instance.setCategoryIds(collection1);
} else {
instance.setCategoryIds(null);
}
} else if ("created_time".equals(fieldName)) {
instance.setCreatedTime(jsonParser.getCurrentToken() == JsonToken.VALUE_NULL ? null : Long.valueOf(jsonParser.getValueAsLong()));
} else if ("id".equals(fieldName)) {
instance.setId(jsonParser.getCurrentToken() == JsonToken.VALUE_NULL ? null : Long.valueOf(jsonParser.getValueAsLong()));
} else if ("image_url".equals(fieldName)) {
instance.setImageUrl(jsonParser.getValueAsString(null));
} else if ("is_favorited".equals(fieldName)) {
instance.setIsFavorited(jsonParser.getCurrentToken() == JsonToken.VALUE_NULL ? null : Boolean.valueOf(jsonParser.getValueAsBoolean()));
} else if ("is_important".equals(fieldName)) {
instance.setIsImportant(jsonParser.getCurrentToken() == JsonToken.VALUE_NULL ? null : Boolean.valueOf(jsonParser.getValueAsBoolean()));
} else if ("is_liked".equals(fieldName)) {
instance.setIsLiked(jsonParser.getCurrentToken() == JsonToken.VALUE_NULL ? null : Boolean.valueOf(jsonParser.getValueAsBoolean()));
} else if ("number_of_likes".equals(fieldName)) {
instance.setNumberOfLikes(jsonParser.getCurrentToken() == JsonToken.VALUE_NULL ? null : Integer.valueOf(jsonParser.getValueAsInt()));
} else if ("result_type_code".equals(fieldName)) {
instance.setResultTypeCode(jsonParser.getValueAsString(null));
} else if ("short_description".equals(fieldName)) {
instance.setShortDescription(jsonParser.getValueAsString(null));
} else if ("title".equals(fieldName)) {
instance.setTitle(jsonParser.getValueAsString(null));
} else if ("type_code".equals(fieldName)) {
instance.setTypeCode(jsonParser.getValueAsString(null));
} else if ("updated_time".equals(fieldName)) {
instance.setUpdatedTime(jsonParser.getCurrentToken() == JsonToken.VALUE_NULL ? null : Long.valueOf(jsonParser.getValueAsLong()));
}
}
@Override
public void serialize(FeedItemBO object, JsonGenerator jsonGenerator, boolean writeStartAndEnd) throws IOException {
if (writeStartAndEnd) {
jsonGenerator.writeStartObject();
}
final List<Long> lslocalcategory_ids = object.getCategoryIds();
if (lslocalcategory_ids != null) {
jsonGenerator.writeFieldName("category_ids");
jsonGenerator.writeStartArray();
for (Long element1 : lslocalcategory_ids) {
if (element1 != null) {
jsonGenerator.writeNumber(element1);
}
}
jsonGenerator.writeEndArray();
}
if (object.getCreatedTime() != null) {
jsonGenerator.writeNumberField("created_time", object.getCreatedTime());
}
if (object.getId() != null) {
jsonGenerator.writeNumberField("id", object.getId());
}
if (object.getImageUrl() != null) {
jsonGenerator.writeStringField("image_url", object.getImageUrl());
}
if (object.getIsFavorited() != null) {
jsonGenerator.writeBooleanField("is_favorited", object.getIsFavorited());
}
if (object.getIsImportant() != null) {
jsonGenerator.writeBooleanField("is_important", object.getIsImportant());
}
if (object.getIsLiked() != null) {
jsonGenerator.writeBooleanField("is_liked", object.getIsLiked());
}
if (object.getNumberOfLikes() != null) {
jsonGenerator.writeNumberField("number_of_likes", object.getNumberOfLikes());
}
if (object.getResultTypeCode() != null) {
jsonGenerator.writeStringField("result_type_code", object.getResultTypeCode());
}
if (object.getShortDescription() != null) {
jsonGenerator.writeStringField("short_description", object.getShortDescription());
}
if (object.getTitle() != null) {
jsonGenerator.writeStringField("title", object.getTitle());
}
if (object.getTypeCode() != null) {
jsonGenerator.writeStringField("type_code", object.getTypeCode());
}
if (object.getUpdatedTime() != null) {
jsonGenerator.writeNumberField("updated_time", object.getUpdatedTime());
}
if (writeStartAndEnd) {
jsonGenerator.writeEndObject();
}
}
}
But I use separate RealmObjects and JsonObjects.
I checked the JSONMapper class
https://raw.githubusercontent.com/bluelinelabs/LoganSquare/6c5ec5281fb58d85a99413b7b6f55e9ef18a6e06/core/src/main/java/com/bluelinelabs/logansquare/JsonMapper.java
And it seems to just use the JSONParer from Jackson:
https://github.com/FasterXML/jackson-core/blob/master/src/main/java/com/fasterxml/jackson/core/JsonParser.java
Not sure if Jackson's JSONParser is using reflection though?
@peterbetos I'm pretty sure the streaming API that LoganSquare uses does not use reflection during the mapping process. That's exactly why it's fast.
@cmelchior just to prove that that's the part that fails,
/**
* The exception that will be thrown in the event that LoganSquare.mapperFor() is
* called with a class that hasn't been declared as a @JsonObject.
*/
public class NoSuchMapperException extends RuntimeException {
public NoSuchMapperException(Class cls) {
super("Class " + cls.getCanonicalName() + " could not be mapped to a JSON object. Perhaps it hasn't been annotated with @JsonObject?");
}
}
The guy above received exactly this message, which means getMapperFor(Class<?>) failed to return a mapper.
Although it's weird because if the proxy class is inherited, then why isn't it auto-generated on second try...?
It probably goes into } catch(Exception e) { /* ignored */ } which isn't very helpful.
I believe somehow you have to put the mappers for the normal object into the LoganSquare mapper map for the .class of the proxies... >.>
getMapperFor() is a static method and the OBJECT_MAPPERS map is private static final, so you'd probably need to use reflection for that and do it manually... -_-
The other alternative is a new TypeConverter for each proxy, but that's even hackier.
Moving annotations to the proxy class might have some unforeseen consequences (for other annotation processors). We will have to look a bit more into it in order to understand the issue.
Most helpful comment
It did work. Thanks :)
But if we're saying that it's not using reflection then I suggest pursuing this issue still :)