Skip to content

Instantly share code, notes, and snippets.

@Miha-x64
Last active June 9, 2022 14:19
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Miha-x64/675cebed8405287d9c10a3d58caa5b64 to your computer and use it in GitHub Desktop.
Save Miha-x64/675cebed8405287d9c10a3d58caa5b64 to your computer and use it in GitHub Desktop.
Gson has serializeNulls() setting but doesn't have dontDeserializeNulls(). This is useful to avoid assigning nulls to properties keeping their default values assigned in constructor instead.
import com.google.gson.Gson;
import com.google.gson.JsonIOException;
import com.google.gson.TypeAdapter;
import com.google.gson.internal.JsonReaderInternalAccess;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import com.google.gson.stream.MalformedJsonException;
import okhttp3.MediaType;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import okio.Buffer;
import retrofit2.Converter;
import retrofit2.Retrofit;
import java.io.*;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
/**
* Provides Retrofit {@linkplain Converter.Factory converter factory}
* and a modified {@linkplain JsonReader JSON reader}
* which skip {@code null} values in objects.
*
* This gist: https://gist.github.com/Miha-x64/675cebed8405287d9c10a3d58caa5b64
*/
public final class GsonNullValueSkipping {
private GsonNullValueSkipping() {}
// com.squareup.retrofit2:converter-gson copy-paste
public static final class ConverterFactory extends Converter.Factory {
private final Gson gson;
public ConverterFactory(Gson gson) {
this.gson = gson;
}
@Override public Converter<ResponseBody, ?> responseBodyConverter(
Type type, Annotation[] annotations, Retrofit retrofit) {
TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
return new GsonResponseBodyConverter<>(gson, adapter);
}
@Override public Converter<?, RequestBody> requestBodyConverter(
Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
return new GsonRequestBodyConverter<>(gson, adapter);
}
}
// com.squareup.retrofit2:converter-gson copy-paste
private static final class GsonRequestBodyConverter<T> implements Converter<T, RequestBody> { // unchanged
private static final MediaType MEDIA_TYPE = MediaType.get("application/json; charset=UTF-8");
private static final Charset UTF_8 = Charset.forName("UTF-8");
private final Gson gson;
private final TypeAdapter<T> adapter;
GsonRequestBodyConverter(Gson gson, TypeAdapter<T> adapter) {
this.gson = gson;
this.adapter = adapter;
}
@Override public RequestBody convert(T value) throws IOException {
Buffer buffer = new Buffer();
Writer writer = new OutputStreamWriter(buffer.outputStream(), UTF_8);
JsonWriter jsonWriter = gson.newJsonWriter(writer);
adapter.write(jsonWriter, value);
jsonWriter.close();
return RequestBody.create(buffer.readByteString(), MEDIA_TYPE);
}
}
// com.squareup.retrofit2:converter-gson copy-paste except constructor
private static final class GsonResponseBodyConverter<T> implements Converter<ResponseBody, T> {
private final boolean lenient;
private final TypeAdapter<T> adapter;
GsonResponseBodyConverter(Gson gson, TypeAdapter<T> adapter) {
JsonReader victim = gson.newJsonReader(new StringReader(/*pass a valid JSON for decency, LOL */ "[]"));
this.lenient = victim.isLenient();
try { victim.close(); } catch (IOException impossible) { throw new AssertionError(impossible); }
this.adapter = adapter;
}
@Override public T convert(ResponseBody value) throws IOException {
JsonReader jsonReader = new NullValueSkippingJsonReader(value.charStream());
jsonReader.setLenient(lenient);
try {
T result = adapter.read(jsonReader);
if (jsonReader.peek() != JsonToken.END_DOCUMENT) {
throw new JsonIOException("JSON document was not fully consumed.");
}
return result;
} finally {
value.close();
}
}
}
@SuppressWarnings({"StringEquality", "StringOperationCanBeSimplified"}) // intentional identity
public static final class NullValueSkippingJsonReader extends JsonReader {
private static final String BETWEEN = new String("standing after a name, before a value");
public NullValueSkippingJsonReader(Reader in) {
super(in);
}
private String nextName;
private long stack; // depth >64 is not supported, he-he
private boolean nextNameAsString = false;
@Override public void beginArray() throws IOException {
unexpect(hasName(), "BEGIN_ARRAY", JsonToken.NAME);
super.beginArray();
push(0);
}
@Override public void endArray() throws IOException {
unexpect(hasName(), "END_ARRAY", JsonToken.NAME);
super.endArray();
pop(0);
}
@Override public void beginObject() throws IOException {
unexpect(hasName(), "BEGIN_OBJECT", JsonToken.NAME);
super.beginObject();
push(1);
}
@Override public void endObject() throws IOException {
unexpect(hasName(), "END_OBJECT", JsonToken.NAME);
super.endObject();
pop(1);
}
private boolean hasName() {
return nextName != null && nextName != BETWEEN;
}
private void push(int isObject) {
if ((stack & 1L<<63) != 0) throw new UnsupportedOperationException("too deep nesting");
stack = (stack << 1) | isObject;
nextName = null; // if was BETWEEN, then now we're inside; else it already was null
}
private void pop(int what) {
if (nextName == BETWEEN || (stack & 1) != what) throw new AssertionError();
stack >>>= 1;
}
@Override public boolean hasNext() throws IOException {
if ((stack & 1) == 0 || nextName == BETWEEN)
return super.hasNext(); // in array or between key and value
if (nextName != null)
return true; // already peeked
return (nextName = findNextName()) != null;
}
@Override public JsonToken peek() throws IOException {
boolean hasName = hasName();
if (hasName && nextNameAsString) return JsonToken.STRING;
return hasName ? JsonToken.NAME : super.peek();
}
@Override public String nextName() throws IOException {
unexpect(nextNameAsString, "a string", JsonToken.NAME);
String name = nextName;
if (name != null) {
unexpect(name == BETWEEN, "a name", super.peek());
nextName = BETWEEN;
return name;
}
unexpect((nextName = findNextName()) == null, "a name", super.peek());
return nextName;
}
@Override public String nextString() throws IOException {
if (pollNextNameAsString()) return nextName();
unexpect(hasName(), "a string", JsonToken.NAME);
String s = super.nextString();
nextName = null;
return s;
}
@Override public boolean nextBoolean() throws IOException {
unexpect(hasName(), "a boolean", JsonToken.NAME);
boolean b = super.nextBoolean();
nextName = null;
return b;
}
@Override public void nextNull() throws IOException {
unexpect(nextName != null, "null", nextName == BETWEEN ? super.peek() : JsonToken.NAME);
super.nextNull();
}
@Override public double nextDouble() throws IOException {
if (pollNextNameAsString()) return nextNameAsDouble();
unexpect(hasName(), "a double", JsonToken.NAME);
double d = super.nextDouble();
nextName = null;
return d;
}
@Override public long nextLong() throws IOException {
if (pollNextNameAsString()) return nextNameAsLong();
unexpect(hasName(), "a long", JsonToken.NAME);
long l = super.nextLong();
nextName = null;
return l;
}
@Override public int nextInt() throws IOException {
if (pollNextNameAsString()) return nextNameAsInt();
unexpect(hasName(), "an int", JsonToken.NAME);
int i = super.nextInt();
nextName = null;
return i;
}
@Override public void skipValue() throws IOException {
JsonToken t;
if (hasName()) {
// nothing special
} else if ((t = super.peek()) == JsonToken.END_ARRAY) {
// both workaround infinite loop and catch up with state
super.endArray(); // could be this.endArray, but we don't need its hasName() check
pop(0);
} else if (t == JsonToken.END_OBJECT) {
super.endObject();
pop(1);
} else {
super.skipValue();
}
nextName = null; // could be either name or BETWEEN
}
private String findNextName() throws IOException {
while (super.hasNext()) {
String name = super.nextName();
if (super.peek() == JsonToken.NULL) {
skipValue();
} else {
return name;
}
}
return null;
}
private void unexpect(boolean condition, String expected, JsonToken unexpected) {
if (condition)
throw new IllegalStateException("Expected " + expected + " but was " + unexpected + locationString());
}
private String locationString() {
return toString().substring(getClass().getSimpleName().length());
}
// DANGER ZONE: degenerative retarded workarounds
void makeRetarded() {
nextNameAsString = true;
}
private boolean pollNextNameAsString() {
if (nextNameAsString) {
nextNameAsString = false;
return true;
}
return false;
}
private double nextNameAsDouble() throws IOException {
double result = Double.parseDouble(nextName()); // don't catch this NumberFormatException.
if (!isLenient() && (Double.isNaN(result) || Double.isInfinite(result)))
throw new MalformedJsonException("JSON forbids NaN and infinities: " + result + locationString());
return result;
}
private long nextNameAsLong() throws IOException {
String peekedString = nextName();
try { return Long.parseLong(peekedString); }
catch (NumberFormatException ignored) {} // Fall back to parse as a double below.
double asDouble = Double.parseDouble(peekedString); // don't catch this NumberFormatException.
long result = (long) asDouble;
if (result != asDouble) // Make sure no precision was lost casting to 'long'.
throw new NumberFormatException("Expected a long but was " + peekedString + locationString());
return result;
}
private int nextNameAsInt() throws IOException {
String peekedString = nextName();
try { return Integer.parseInt(peekedString); }
catch (NumberFormatException ignored) {} // Fall back to parse as a double below.
double asDouble = Double.parseDouble(peekedString); // don't catch this NumberFormatException.
int result = (int) asDouble;
if (result != asDouble) // Make sure no precision was lost casting to 'int'.
throw new NumberFormatException("Expected an int but was " + peekedString + locationString());
return result;
}
static {
JsonReaderInternalAccess тупорылыйДегенератскийКостыльДляУебанскогоMapTypeAdapterFucktory =
JsonReaderInternalAccess.INSTANCE;
JsonReaderInternalAccess.INSTANCE = new JsonReaderInternalAccess() {
@Override public void promoteNameToValue(JsonReader reader) throws IOException {
if (reader instanceof NullValueSkippingJsonReader)
((NullValueSkippingJsonReader) reader).makeRetarded();
else
тупорылыйДегенератскийКостыльДляУебанскогоMapTypeAdapterFucktory.promoteNameToValue(reader);
}
};
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment