¿Por qué aparece el mensaje "Error al rebotar para escribir" cuando convierto JSON de Firebase en objetos Java?
[Divulgación: soy ingeniero en Firebase. Esta pregunta pretende ser una pregunta de referencia para responder muchas preguntas de una vez.]
Tengo la siguiente estructura JSON en mi base de datos de Firebase:
{
"users": {
"-Jx5vuRqItEF-7kAgVWy": {
"handle": "puf",
"name": "Frank van Puffelen",
"soId": 209103
},
"-Jx5w3IOHD2kRFFgkMbh": {
"handle": "kato",
"name": "Kato Wulf",
"soId": 394010
},
"-Jx5x1VWs08Zc5S-0U4p": {
"handle": "mimming",
"name": "Jenny Tong",
"soId": 839465
}
}
}
Estoy leyendo esto con el siguiente código:
private static class User {
String handle;
String name;
public String getHandle() { return handle; }
public String getName() { return name; }
}
Firebase ref = new Firebase("https://stackoverflow.firebaseio.com/32108969/users");
ref.addListenerForSingleValueEvent(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot usersSnapshot) {
for (DataSnapshot userSnapshot : usersSnapshot.getChildren()) {
User user = userSnapshot.getValue(User.class);
System.out.println(user.toString());
}
}
@Override
public void onCancelled(FirebaseError firebaseError) { }
});
Pero me sale este error:
Excepción en el hilo "FirebaseEventTarget" com.firebase.client.FirebaseException: no se pudo rebotar para escribir
¿Cómo puedo leer a mis usuarios en objetos Java?
Firebase usa Jackson para permitir la serialización de objetos Java a JSON y la deserialización de JSON nuevamente a objetos Java. Puede encontrar más información sobre Jackson en el sitio web de Jackson y en esta página sobre anotaciones de Jackson .
En el resto de esta respuesta, mostraremos algunas formas comunes de usar Jackson con Firebase.
Cargando usuarios completos
La forma más sencilla de cargar los usuarios de Firebase a Android es si creamos una clase Java que imite completamente las propiedades del JSON:
private static class User {
String handle;
String name;
long stackId;
public String getHandle() { return handle; }
public String getName() { return name; }
public long getStackId() { return stackId; }
@Override
public String toString() { return "User{handle='"+handle+“', name='"+name+"', stackId="+stackId+"\’}”; }
}
Podemos usar esta clase en un oyente:
Firebase ref = new Firebase("https://stackoverflow.firebaseio.com/32108969/users");
ref.addListenerForSingleValueEvent(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot usersSnapshot) {
for (DataSnapshot userSnapshot : usersSnapshot.getChildren()) {
User user = userSnapshot.getValue(User.class);
System.out.println(user.toString());
}
}
@Override
public void onCancelled(FirebaseError firebaseError) { }
});
Puede observar que la clase Usuario sigue el patrón de propiedad JavaBean . Cada propiedad JSON se asigna a un campo en la clase Usuario y tenemos un método getter público para cada campo. Al asegurarnos de que todas las propiedades estén asignadas con exactamente el mismo nombre, nos aseguramos de que Jackson pueda asignarlas automáticamente.
También puede controlar manualmente el mapeo colocando anotaciones de Jackson en su clase Java, sus campos y métodos. Cubriremos las dos anotaciones más comunes ( @JsonIgnore
y @JsonIgnoreProperties
) a continuación.
Cargando usuarios parcialmente
Digamos que solo le importa el nombre del usuario y el identificador en su código Java. Eliminemos el stackId
y veamos qué pasa:
private static class User {
String handle;
String name;
public String getHandle() { return handle; }
public String getName() { return name; }
@Override
public String toString() {
return "User{handle='" + handle + “\', name='" + name + "\’}”;
}
}
Si ahora adjuntamos el mismo oyente que antes y ejecutamos el programa, generará una excepción:
Exception in thread "FirebaseEventTarget" com.firebase.client.FirebaseException: Failed to bounce to type
at com.firebase.client.DataSnapshot.getValue(DataSnapshot.java:187)
at com.firebase.LoadPartialUsers$1.onDataChange(LoadPartialUsers.java:16)
El "tipo de rebote fallido" indica que Jackson no pudo deserializar el JSON en un objeto de Usuario. En la excepción anidada nos dice por qué:
Caused by: com.shaded.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "stackId" (class com.firebase.LoadPartialUsers$User), not marked as ignorable (2 known properties: , "handle", "name"])
at [Source: java.io.StringReader@43079089; line: 1, column: 15] (through reference chain: com.firebase.User["stackId"])
at com.shaded.fasterxml.jackson.databind.exc.UnrecognizedPropertyException.from(UnrecognizedPropertyException.java:79)
Jackson encontró una propiedad stackId
en JSON y no sabe qué hacer con ella, por lo que genera una excepción. Afortunadamente, hay una anotación que podemos usar para indicarle que ignore propiedades específicas del JSON al asignarlo a nuestra User
clase:
@JsonIgnoreProperties({ "stackId" })
private static class User {
...
}
Si no volvemos a ejecutar el código con nuestro oyente, Jackson sabrá que puede ignorarlo stackId
en el JSON y podrá deserializar el JSON en un objeto Usuario nuevamente.
Dado que agregar propiedades al JSON es una práctica común en las aplicaciones de Firebase, puede que le resulte más conveniente simplemente decirle a Jackson que ignore todas las propiedades que no tienen una asignación en la clase Java:
@JsonIgnoreProperties(ignoreUnknown=true)
private static class User {
...
}
Ahora, si agregamos propiedades al JSON más adelante, el código Java aún podrá cargar el User
s. Solo tenga en cuenta que los objetos Usuario no contendrán toda la información que estaba presente en el JSON, así que tenga cuidado al volver a escribirlos en Firebase.
Salvando parcialmente a los usuarios
Una de las razones por las que es bueno tener una clase Java personalizada es que podemos agregarle métodos convenientes. Digamos que agregamos un método conveniente que hace que el nombre se muestre a un usuario:
private static class User {
String handle;
String name;
public String getHandle() { return handle; }
public String getName() { return name; }
@JsonIgnore
public String getDisplayName() {
return getName() + " (" + getHandle() + ")";
}
@Override
public String toString() {
return "User{handle='" + handle + "\', name='" + name + "\', displayName='" + getDisplayName() + "'}";
}
}
Ahora leamos los usuarios de Firebase y volvamos a escribirlos en una nueva ubicación:
Firebase srcRef = new Firebase("https://stackoverflow.firebaseio.com/32108969/users");
final Firebase copyRef = new Firebase("https://stackoverflow.firebaseio.com/32108969/copiedusers");
srcRef.addListenerForSingleValueEvent(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot usersSnapshot) {
for (DataSnapshot userSnapshot : usersSnapshot.getChildren()) {
User user = userSnapshot.getValue(User.class);
copyRef.child(userSnapshot.getKey()).setValue(user);
}
}
@Override
public void onCancelled(FirebaseError firebaseError) { }
});
El JSON en el copiedusers
nodo tiene este aspecto:
"copiedusers": {
"-Jx5vuRqItEF-7kAgVWy": {
"displayName": "Frank van Puffelen (puf)",
"handle": "puf",
"name": "Frank van Puffelen"
},
"-Jx5w3IOHD2kRFFgkMbh": {
"displayName": "Kato Wulf (kato)",
"handle": "kato",
"name": "Kato Wulf"
},
"-Jx5x1VWs08Zc5S-0U4p": {
"displayName": "Jenny Tong (mimming)",
"handle": "mimming",
"name": "Jenny Tong"
}
}
No es lo mismo que el JSON fuente, porque Jackson reconoce el nuevo getDisplayName()
método como un captador JavaBean y, por lo tanto, agrega una displayName
propiedad al JSON que genera. Resolvemos este problema agregando una JsonIgnore
anotación a getDisplayName()
.
@JsonIgnore
public String getDisplayName() {
return getName() + "(" + getHandle() + ")";
}
Al serializar un objeto Usuario, Jackson ahora ignorará el getDisplayName()
método y el JSON que escribimos será el mismo que obtuvimos.
Las versiones 9.x (y superiores) del SDK de Firebase para Android/Java dejaron de incluir Jackson para serializar/deserializar Java<->JSON. En cambio, el SDK más nuevo proporciona un conjunto mínimo de anotaciones personalizadas para permitir el control sobre las necesidades de personalización más comunes, al tiempo que tiene un impacto mínimo en el tamaño JAR/APK resultante.
Mi respuesta original sigue siendo válida si eres:
- Uso de los SDK de Firebase 2.x
- Usar los SDK de Firebase 9.0 o superiores, pero usar Jackson para serializar/deserializar Java<->JSON .
El resto de esta respuesta cubre cómo manejar escenarios de serialización/deserialización en Firebase SDK 9.0 o superior.
Estructura de datos
Comenzaremos con esta estructura JSON en nuestra base de datos Firebase:
{
"-Jx86I5e8JBMZ9tH6W3Q" : {
"handle" : "puf",
"name" : "Frank van Puffelen",
"stackId" : 209103,
"stackOverflowId" : 209103
},
"-Jx86Ke_fk44EMl8hRnP" : {
"handle" : "mimming",
"name" : "Jenny Tong",
"stackId" : 839465
},
"-Jx86N4qeUNzThqlSMer" : {
"handle" : "kato",
"name" : "Kato Wulf",
"stackId" : 394010
}
}
Cargando usuarios completos
En su forma más básica, podemos cargar cada usuario desde este JSON en la siguiente clase Java:
private static class CompleteUser {
String handle;
String name;
long stackId;
public String getHandle() { return handle; }
public String getName() { return name; }
public long getStackId() { return stackId; }
@Override
public String toString() { return "User{handle='"+handle+"', name='"+name+"', stackId="+stackId+ "'}"; }
}
Si declaramos que los campos son públicos, ni siquiera necesitamos los captadores:
private static class CompleteUser {
public String handle;
public String name;
public long stackId;
}
Cargando usuarios parcialmente
También podemos cargar parcialmente un usuario, por ejemplo con:
private static class PartialUser {
String handle;
String name;
public String getHandle() {
return handle;
}
public String getName() { return name; }
@Override
public String toString() {
return "User{handle='" + handle + "', NAME='" + name + "''}";
}
}
Cuando usamos esta clase para cargar los usuarios desde el mismo JSON, el código se ejecuta (a diferencia de la variante Jackson mencionada en mi otra respuesta). Pero verás una advertencia en el resultado de tu registro:
ADVERTENCIA: No se encontró ningún definidor/campo para stackId en la clase Annotations$PartialUser
Entonces deshazte de eso, podemos anotar la clase con @IgnoreExtraProperties
:
@IgnoreExtraProperties
private static class PartialUser {
String handle;
String name;
public String getHandle() {
return handle;
}
public String getName() { return name; }
@Override
public String toString() {
return "User{handle='" + handle + "', NAME='" + name + "''}";
}
}
Salvando parcialmente a los usuarios
Como antes, es posible que desees agregar una propiedad calculada al usuario. Debería ignorar dicha propiedad al guardar los datos en la base de datos. Para hacer esto, puede anotar la propiedad/getter/setter/campo con @Exclude
:
private static class OvercompleteUser {
String handle;
String name;
long stackId;
public String getHandle() { return handle; }
public String getName() { return name; }
public long getStackId() { return stackId; }
@Exclude
public String getTag() { return getName() + " ("+getHandle()+")"; }
@Override
public String toString() { return "User{handle='"+handle+"', name='"+name+"', stackId="+stackId+ "'}"; }
}
Ahora, al escribir un usuario en la base de datos, getTag()
se ignorará el valor de.
Usar un nombre de propiedad diferente en JSON que en el código Java
También puede especificar qué nombre debe obtener un campo/captador/definidor del código Java en el JSON de la base de datos. Para hacer esto: anote el campo/getter/setter con @PropertyName()
.
private static class UserWithRenamedProperty {
String handle;
String name;
@PropertyName("stackId")
long stackOverflowId;
public String getHandle() { return handle; }
public String getName() { return name; }
@PropertyName("stackId")
public long getStackOverflowId() { return stackOverflowId; }
@Override
public String toString() { return "User{handle='"+handle+"', name='"+name+"', stackId="+stackOverflowId+ "'}"; }
}
En general, es mejor utilizar la asignación predeterminada entre Java<->JSON que utiliza el SDK de Firebase. Pero @PropertyName
puede ser necesario cuando tiene una estructura JSON preexistente que de otro modo no puede asignar a clases de Java.