Дополнительные поля и обязательные поля Gson
Как следует иметь дело с Gson
и обязательными или необязательными полями?
Поскольку все поля являются необязательными, я не могу действительно выполнить мой сетевой запрос, исходя из того, что ответ json содержит некоторый ключ, Gson
просто проанализирует его на null.
Метод Я использую gson.fromJson(json, mClassOfT);
Например, если у меня есть следующий json:
{"user_id":128591, "user_name":"TestUser"}
И мой класс:
public class User {
@SerializedName("user_id")
private String mId;
@SerializedName("user_name")
private String mName;
public String getId() {
return mId;
}
public void setId(String id) {
mId = id;
}
public String getName() {
return mName;
}
public void setName(String name) {
mName = name;
}
}
Является ли какой-либо способ получить Gson
для отказа, если json не будет содержать ключ user_id
или user_name
?
Может быть много случаев, когда вам могут понадобиться хотя бы некоторые значения для анализа, а другие - необязательные?
Существует ли какой-либо шаблон или библиотека для обработки этого случая по всему миру?
Спасибо.
Ответы
Ответ 1
Как вы заметили, у Gson нет возможности определить "обязательное поле", и вы просто получите null
в своем десериализованном объекте, если в JSON чего-то не хватает.
Здесь используется повторно используемый десериализатор и аннотация, которые будут делать это. Ограничение состоит в том, что если POJO требовал настраиваемого десериализатора как есть, вам нужно было бы пойти немного дальше и либо передать объект Gson
в конструкторе, чтобы десериализовать объект, либо переместить аннотацию, проверяя ее на отдельный и используйте его в своем десериализаторе. Вы также можете улучшить обработку исключений, создав собственное исключение и передав его в JsonParseException
, чтобы его можно было обнаружить через getCause()
в вызывающем.
Все сказанное в подавляющем большинстве случаев будет работать:
public class App
{
public static void main(String[] args)
{
Gson gson =
new GsonBuilder()
.registerTypeAdapter(TestAnnotationBean.class, new AnnotatedDeserializer<TestAnnotationBean>())
.create();
String json = "{\"foo\":\"This is foo\",\"bar\":\"this is bar\"}";
TestAnnotationBean tab = gson.fromJson(json, TestAnnotationBean.class);
System.out.println(tab.foo);
System.out.println(tab.bar);
json = "{\"foo\":\"This is foo\"}";
tab = gson.fromJson(json, TestAnnotationBean.class);
System.out.println(tab.foo);
System.out.println(tab.bar);
json = "{\"bar\":\"This is bar\"}";
tab = gson.fromJson(json, TestAnnotationBean.class);
System.out.println(tab.foo);
System.out.println(tab.bar);
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface JsonRequired
{
}
class TestAnnotationBean
{
@JsonRequired public String foo;
public String bar;
}
class AnnotatedDeserializer<T> implements JsonDeserializer<T>
{
public T deserialize(JsonElement je, Type type, JsonDeserializationContext jdc) throws JsonParseException
{
T pojo = new Gson().fromJson(je, type);
Field[] fields = pojo.getClass().getDeclaredFields();
for (Field f : fields)
{
if (f.getAnnotation(JsonRequired.class) != null)
{
try
{
f.setAccessible(true);
if (f.get(pojo) == null)
{
throw new JsonParseException("Missing field in JSON: " + f.getName());
}
}
catch (IllegalArgumentException ex)
{
Logger.getLogger(AnnotatedDeserializer.class.getName()).log(Level.SEVERE, null, ex);
}
catch (IllegalAccessException ex)
{
Logger.getLogger(AnnotatedDeserializer.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
return pojo;
}
}
Выход:
This is foo
this is bar
This is foo
null
Exception in thread "main" com.google.gson.JsonParseException: Missing field in JSON: foo
Ответ 2
Это мое простое решение, которое создает общее решение с минимальным кодированием.
- Создать @Optional аннотацию
- Отметить первый вариант. Отдых считается необязательным. Предполагается, что раньше предполагается.
- Создайте общий метод "загрузчик", который проверяет, что исходный объект Json имеет значение. Цикл останавливается, когда встречается поле @Optional.
Я использую подклассу, поэтому работа с ворчанием выполняется в суперклассе.
Вот код суперкласса.
import com.google.gson.Gson;
import java.lang.reflect.Field;
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
...
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Optional {
public boolean enabled() default true;
}
и метод работы grunt
@SuppressWarnings ("unchecked")
public <T> T payload(JsonObject oJR,Class<T> T) throws Exception {
StringBuilder oSB = new StringBuilder();
String sSep = "";
Object o = gson.fromJson(oJR,T);
// Ensure all fields are populated until we reach @Optional
Field[] oFlds = T.getDeclaredFields();
for(Field oFld:oFlds) {
Annotation oAnno = oFld.getAnnotation(Optional.class);
if (oAnno != null) break;
if (!oJR.has(oFld.getName())) {
oSB.append(sSep+oFld.getName());
sSep = ",";
}
}
if (oSB.length() > 0) throw CVT.e("Required fields "+oSB+" mising");
return (T)o;
}
и пример использования
public static class Payload {
String sUserType ;
String sUserID ;
String sSecpw ;
@Optional
String sUserDev ;
String sUserMark ;
}
и заполняющий код
Payload oPL = payload(oJR,Payload.class);
В этом случае sUserDev и sUserMark являются необязательными, а остальные - обязательными. Решение основывается на том, что класс хранит определения полей в заявленном порядке.
Ответ 3
Я много искал и не нашел хорошего ответа. Решение, которое я выбрал, выглядит следующим образом:
Каждое поле, которое мне нужно установить из JSON, представляет собой объект, то есть вложенный в квадрат Integer, Boolean и т.д. Затем, используя отражение, я могу проверить, что поле не равно null:
public class CJSONSerializable {
public void checkDeserialization() throws IllegalAccessException, JsonParseException {
for (Field f : getClass().getDeclaredFields()) {
if (f.get(this) == null) {
throw new JsonParseException("Field " + f.getName() + " was not initialized.");
}
}
}
}
Из этого класса я могу получить свой объект JSON:
public class CJSONResp extends CJSONSerializable {
@SerializedName("Status")
public String status;
@SerializedName("Content-Type")
public String contentType;
}
а затем после разбора с помощью GSON я могу вызвать checkDeserialization, и он сообщит мне, если некоторые из полей являются нулевыми.
Ответ 4
Ответа на вопрос Брайана Роуча очень хорошо, но иногда ему также необходимо обращаться:
- свойства суперкласса модели
- свойства внутри массивов
Для этих целей может использоваться следующий класс:
/**
* Adds the feature to use required fields in models.
*
* @param <T> Model to parse to.
*/
public class JsonDeserializerWithOptions<T> implements JsonDeserializer<T> {
/**
* To mark required fields of the model:
* json parsing will be failed if these fields won't be provided.
* */
@Retention(RetentionPolicy.RUNTIME) // to make reading of this field possible at the runtime
@Target(ElementType.FIELD) // to make annotation accessible throw the reflection
public @interface FieldRequired {}
/**
* Called when the model is being parsed.
*
* @param je Source json string.
* @param type Object model.
* @param jdc Unused in this case.
*
* @return Parsed object.
*
* @throws JsonParseException When parsing is impossible.
* */
@Override
public T deserialize(JsonElement je, Type type, JsonDeserializationContext jdc)
throws JsonParseException {
// Parsing object as usual.
T pojo = new Gson().fromJson(je, type);
// Getting all fields of the class and checking if all required ones were provided.
checkRequiredFields(pojo.getClass().getDeclaredFields(), pojo);
// Checking if all required fields of parent classes were provided.
checkSuperClasses(pojo);
// All checks are ok.
return pojo;
}
/**
* Checks whether all required fields were provided in the class.
*
* @param fields Fields to be checked.
* @param pojo Instance to check fields in.
*
* @throws JsonParseException When some required field was not met.
* */
private void checkRequiredFields(@NonNull Field[] fields, @NonNull Object pojo)
throws JsonParseException {
// Checking nested list items too.
if (pojo instanceof List) {
final List pojoList = (List) pojo;
for (final Object pojoListPojo : pojoList) {
checkRequiredFields(pojoListPojo.getClass().getDeclaredFields(), pojoListPojo);
checkSuperClasses(pojoListPojo);
}
}
for (Field f : fields) {
// If some field has required annotation.
if (f.getAnnotation(FieldRequired.class) != null) {
try {
// Trying to read this field value and check that it truly has value.
f.setAccessible(true);
Object fieldObject = f.get(pojo);
if (fieldObject == null) {
// Required value is null - throwing error.
throw new JsonParseException(String.format("%1$s -> %2$s",
pojo.getClass().getSimpleName(),
f.getName()));
} else {
checkRequiredFields(fieldObject.getClass().getDeclaredFields(), fieldObject);
checkSuperClasses(fieldObject);
}
}
// Exceptions while reflection.
catch (IllegalArgumentException | IllegalAccessException e) {
throw new JsonParseException(e);
}
}
}
}
/**
* Checks whether all super classes have all required fields.
*
* @param pojo Object to check required fields in its superclasses.
*
* @throws JsonParseException When some required field was not met.
* */
private void checkSuperClasses(@NonNull Object pojo) throws JsonParseException {
Class<?> superclass = pojo.getClass();
while ((superclass = superclass.getSuperclass()) != null) {
checkRequiredFields(superclass.getDeclaredFields(), pojo);
}
}
}
Прежде всего описывается интерфейс (аннотация) для отметки обязательных полей, мы увидим пример его использования позже:
/**
* To mark required fields of the model:
* json parsing will be failed if these fields won't be provided.
* */
@Retention(RetentionPolicy.RUNTIME) // to make reading of this field possible at the runtime
@Target(ElementType.FIELD) // to make annotation accessible throw the reflection
public @interface FieldRequired {}
Затем реализуется метод deserialize
. Он, как обычно, анализирует строки json: отсутствующие свойства в результате pojo
будут иметь значения null
:
T pojo = new Gson().fromJson(je, type);
Затем запускается рекурсивная проверка всех полей разбора pojo
:
checkRequiredFields(pojo.getClass().getDeclaredFields(), pojo);
Затем мы также проверяем все поля суперклассов pojo
:
checkSuperClasses(pojo);
Требуется, когда некоторый SimpleModel
расширяет свой SimpleParentModel
, и мы хотим удостовериться, что все свойства SimpleModel
, отмеченные как требуемые, представлены как SimpleParentModel
.
Посмотрим на метод checkRequiredFields
. Прежде всего, он проверяет, является ли какое-либо свойство экземпляром List
(json array) - в этом случае все объекты списка также должны быть проверены, чтобы убедиться, что они также имеют все обязательные поля:
if (pojo instanceof List) {
final List pojoList = (List) pojo;
for (final Object pojoListPojo : pojoList) {
checkRequiredFields(pojoListPojo.getClass().getDeclaredFields(), pojoListPojo);
checkSuperClasses(pojoListPojo);
}
}
Затем мы выполняем итерацию по всем полям pojo
, проверяя, имеются ли все поля с аннотацией FieldRequired
(что означает, что эти поля не являются нулевыми). Если мы столкнулись с некоторым нулевым свойством, которое требуется - исключение будет запущено. В противном случае для текущего поля будет запущен другой рекурсивный шаг проверки, а свойства родительских классов поля также будут проверены:
for (Field f : fields) {
// If some field has required annotation.
if (f.getAnnotation(FieldRequired.class) != null) {
try {
// Trying to read this field value and check that it truly has value.
f.setAccessible(true);
Object fieldObject = f.get(pojo);
if (fieldObject == null) {
// Required value is null - throwing error.
throw new JsonParseException(String.format("%1$s -> %2$s",
pojo.getClass().getSimpleName(),
f.getName()));
} else {
checkRequiredFields(fieldObject.getClass().getDeclaredFields(), fieldObject);
checkSuperClasses(fieldObject);
}
}
// Exceptions while reflection.
catch (IllegalArgumentException | IllegalAccessException e) {
throw new JsonParseException(e);
}
}
}
И последний метод должен быть рассмотрен checkSuperClasses
: он просто запускает аналогичные свойства проверки проверки правильности полей суперклассов pojo
:
Class<?> superclass = pojo.getClass();
while ((superclass = superclass.getSuperclass()) != null) {
checkRequiredFields(superclass.getDeclaredFields(), pojo);
}
И, наконец, рассмотрим пример использования JsonDeserializerWithOptions
. Предположим, что мы имеем следующие модели:
private class SimpleModel extends SimpleParentModel {
@JsonDeserializerWithOptions.FieldRequired Long id;
@JsonDeserializerWithOptions.FieldRequired NestedModel nested;
@JsonDeserializerWithOptions.FieldRequired ArrayList<ListModel> list;
}
private class SimpleParentModel {
@JsonDeserializerWithOptions.FieldRequired Integer rev;
}
private class NestedModel extends NestedParentModel {
@JsonDeserializerWithOptions.FieldRequired Long id;
}
private class NestedParentModel {
@JsonDeserializerWithOptions.FieldRequired Integer rev;
}
private class ListModel {
@JsonDeserializerWithOptions.FieldRequired Long id;
}
Мы можем быть уверены, что SimpleModel
будет корректно анализироваться без исключений таким образом:
final Gson gson = new GsonBuilder()
.registerTypeAdapter(SimpleModel.class, new JsonDeserializerWithOptions<SimpleModel>())
.create();
gson.fromJson("{\"list\":[ { \"id\":1 } ], \"id\":1, \"rev\":22, \"nested\": { \"id\":2, \"rev\":2 }}", SimpleModel.class);
Конечно, при условии, что решение может быть улучшено и принимать дополнительные функции: например, проверки для вложенных объектов, которые не помечены аннотацией FieldRequired
. В настоящее время он выходит из области ответа, но может быть добавлен позже.