theAnonymous
theAnonymous

Reputation: 1804

Gson - how to special processing like masking

How to configure Gson to do additional processing on the value for toJson?

public class MyClass{
    @SerializedName("qwerty")
    @Mask(exposeFront=2, exposeRear=2, mask="*")
    private String qwerty
}

Assuming MyClass#qwerty has a value of 1234567890, how to set Gson to output {"qwerty":"12******90"}?

Upvotes: 1

Views: 767

Answers (1)

Gson ReflectiveTypeAdapterFactory, that is responsible for "plain" objects serialization and deserialization, is not possible to enhance to support any other annotations like @Masked. It can only use annotations like @Expose (indirectly via an exclusion strategy), @SerializedName and a few others like @Since and @Until (exclusion strategy too). Note these annotations are documented and supported by default. In general, Gson suggests using a type adapter for the declaring class, MyClass, but this also means that you must manage all fields and make sure the corresponding type adapter is updated once your class is changed. Even worse, adding a custom type adapter makes these annotations support lost.

As an another way of working around it is injecting a special string type adapter factory that can do the trick, but due to the mechanics of how it is injected, this is both limited and requires duplicating the @Masked annotation values (if you're using the annotation elsewhere in your code) and the type adapter factory configuration in @JsonAdapter.

public abstract class MaskedTypeAdapterFactory
        implements TypeAdapterFactory {

    private final int exposeFront;
    private final int exposeRear;
    private final char mask;

    private MaskedTypeAdapterFactory(final int exposeFront, final int exposeRear, final char mask) {
        this.exposeFront = exposeFront;
        this.exposeRear = exposeRear;
        this.mask = mask;
    }

    // must be "baked" into the class (name only represents the configuration)
    public static final class _2_2_asterisk
            extends MaskedTypeAdapterFactory {

        private _2_2_asterisk() {
            super(2, 2, '*');
        }

    }

    @Override
    @Nullable
    public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
        if ( typeToken.getRawType() != String.class ) {
            return null;
        }
        @SuppressWarnings("unchecked")
        final TypeAdapter<String> delegate = (TypeAdapter<String>) gson.getAdapter(typeToken);
        final TypeAdapter<String> typeAdapter = new TypeAdapter<String>() {
            @Override
            public void write(final JsonWriter out, final String value)
                    throws IOException {
                // mask the value
                final int length = value.length();
                final char[] buffer = value.toCharArray();
                for ( int i = exposeFront; i < length - exposeRear; i++ ) {
                    buffer[i] = mask;
                }
                out.value(new String(buffer));
            }

            @Override
            public String read(final JsonReader in)
                    throws IOException {
                return delegate.read(in);
            }
        }
                .nullSafe();
        @SuppressWarnings("unchecked")
        final TypeAdapter<T> adapter = (TypeAdapter<T>) typeAdapter;
        return adapter;
    }

}
@NoArgsConstructor
@AllArgsConstructor
final class MyClass {

    @SerializedName("qwerty")
    @Mask(exposeFront = 2, exposeRear = 2, mask = "*")
    // unfortunately, this must duplicate the @Mask annotation values
    // since type adapter (factories) do not accept supplemental information
    // and Java annotations can only accept compile-time constants
    @JsonAdapter(MaskedTypeAdapterFactory._2_2_asterisk.class)
    @SuppressWarnings("unused")
    private String qwerty;

}

Test:

public final class MaskedTypeAdapterFactoryTest {

    private static final Gson gson = new GsonBuilder()
            .disableHtmlEscaping()
            .disableInnerClassSerialization()
            .create();

    @Test
    public void test() {
        final String actual = gson.toJson(new MyClass("1234567890"));
        final String expected = "{\"qwerty\":\"12******90\"}";
        Assertions.assertEquals(expected, actual);
    }

}

This is probably the most robust way of doing that in Gson.

Upvotes: 1

Related Questions