Google HTTP Client, a.k.a. com.google.http-client:google-http-client, uses jackson by default. If your stack already provides a JSON API/implementation which is not jackson or not the right version you can desire to align both. A typical example if a Microprofile or JakartaEE application which will rely on JSON-P but not jackson directly.

So is the only solution to import jackson "transitively"? Actually not. When google implemented its own object mapper on top of Jackson it also abstracted the link between Google and Jackson so you can bridge JSON-P with Google Json API providing your own JsonFactory.

The JsonFactory class provides to the Google SDK the way to parse and generate data. In other words it is very close to a JsonParser and JsonGenerator of JSON-P with a few differences.

The JsonFactory implementation

The first step to plug JSON-P is to extend JsonFactory from Google SDK:

import com.google.api.client.json.JsonFactory;

public class JsonpJsonFactory extends JsonFactory {

Then we will need to implement parser and generator methods so we need a JSON-P JsonParserFactory and a JsonGeneratorFactory. We use factories but we could also rely on Json global factories methods. It is just saner to use factories because then you can customize and reuse the internal cache on a per usage basis instead of inheriting of the global context which can become a nightmare in an uncontrolled environment - JakataEE, OSGi, ...

The generator part is quite particular because Google SDK has a special hook to "enable pretty formatting" of the JSON. There is no such hook in JSON-P, you have a generator factory which has pretty mode activate or not. To solve that we will just use two factories which will enable us to switch of mode respecting the Google SDK API. This is not a big deal since it is just a matter of having another object.

Therefore at that stage here is our constructor and instantiation methods (more Google style friendly):

public class JsonpJsonFactory extends JsonFactory {
    private final JsonParserFactory parserFactory;
    private final JsonGeneratorFactory generatorFactory;
    private final JsonGeneratorFactory prettyGeneratorFactory;

    private JsonpJsonFactory(final JsonParserFactory parserFactory,
                             final JsonGeneratorFactory generatorFactory,
                             final JsonGeneratorFactory prettyGeneratorFactory) {
        this.parserFactory = parserFactory;
        this.generatorFactory = generatorFactory;
        this.prettyGeneratorFactory = prettyGeneratorFactory;
    }

    public static JsonFactory of() {
        return of(
                Json.createParserFactory(emptyMap()),
                Json.createGeneratorFactory(emptyMap()),
                Json.createGeneratorFactory(singletonMap(javax.json.stream.JsonGenerator.PRETTY_PRINTING, "true")));
    }

    public static JsonFactory of(final JsonParserFactory parserFactory, final JsonGeneratorFactory generatorFactory) {
        return new JsonpJsonFactory(parserFactory, generatorFactory, generatorFactory);
    }

    public static JsonFactory of(final JsonParserFactory parserFactory,
                                 final JsonGeneratorFactory generatorFactory,
                                 final JsonGeneratorFactory prettyGeneratorFactory) {
        return new JsonpJsonFactory(parserFactory, generatorFactory, prettyGeneratorFactory);
    }
}

You can notice the way we get a pretty generator factory in the first of() method.

Now our class has all we need to fill the Google JsonFactory API method let's start it. We will start by the easiest side: the parser. On that side we will create a Google SDK JsonParser implementation (take care Google and Json-P used the same simple name) and pass it a JSON-P instance which will be used under the cover:

public class JsonpJsonFactory extends JsonFactory { // <1>
    // ...

    @Override
    public JsonParser createJsonParser(final InputStream inputStream) {
        return new JsonParserImpl(this, parserFactory.createParser(requireNonNull(inputStream)));
    }

    @Override
    public JsonParser createJsonParser(final InputStream inputStream, final Charset charset) {
        return new JsonParserImpl(this, parserFactory.createParser(requireNonNull(inputStream), charset));
    }

    @Override
    public JsonParser createJsonParser(final String s) {
        return new JsonParserImpl(this, parserFactory.createParser(new StringReader(requireNonNull(s))));
    }

    @Override
    public JsonParser createJsonParser(final Reader reader) {
        return new JsonParserImpl(this, parserFactory.createParser(requireNonNull(reader)));
    }
}

Assuming we have the JsonParserImpl - and we will cover that step in next part - the implementation here is just a plain delegation since all the methods exist in the JsonParserFactory of JSON-P.

The generator side is really close except the activation of the pretty mode must be done lazily - it is called on the generator itself - so we'll create a Google SDK JsonGenerator implementation bridging to the JSON-P JsonGenerator and this implementation will take a Function<Boolean, JsonGenerator> which will enable us to pass to this factory function if the pretty mode is activate or not and use the right JsonGeneratorFactory accordingly:

public class JsonpJsonFactory extends JsonFactory {
    // ...

    @Override
    public JsonGenerator createJsonGenerator(final OutputStream outputStream, final Charset charset) {
        return new JsonGeneratorImpl(this,
                pretty -> getGeneratorFactory(pretty).createGenerator(outputStream, charset));
    }

    @Override
    public JsonGenerator createJsonGenerator(final Writer writer) {
        return new JsonGeneratorImpl(this,
                pretty -> getGeneratorFactory(pretty).createGenerator(writer));
    }

    private JsonGeneratorFactory getGeneratorFactory(final boolean pretty) {
        return pretty ? prettyGeneratorFactory : generatorFactory;
    }
}

Now all we miss are the bridging implementations we will cover in the two next parts.

The JsonParser bridge

The parser bridge has very few specificities under the delegation pattern:

  • The JSON-P Event representing a token of the grammar must be converted to a Google SDK JsonToken,
  • nextToken() must implicitly handle hasNext() to return the right token,
  • skipChilden() must handle ARRAY and OBJECT cases in a single method.

All other methods are pure delegation and sometimes exception mapping since JsonException is not part of Google SDK - in general it will be IllegalArgumentException:

class JsonParserImpl extends JsonParser {
    private final JsonFactory factory;
    private final javax.json.stream.JsonParser delegate;

    private JsonToken current = null;

    JsonParserImpl(final JsonFactory factory,
                          final javax.json.stream.JsonParser delegate) {
        this.factory = factory;
        this.delegate = delegate;
    }

    @Override
    public JsonFactory getFactory() {
        return factory;
    }

    @Override
    public void close() {
        delegate.close();
    }

    @Override
    public JsonToken nextToken() {
        if (!delegate.hasNext()) {
            return current = null;
        }
        try {
            return current = mapToken(delegate.next());
        } catch (final ArrayIndexOutOfBoundsException | JsonParsingException e) {
            throw new IllegalArgumentException(e);
        }
    }

    @Override
    public JsonToken getCurrentToken() {
        return current;
    }

    @Override
    public String getCurrentName() {
        try {
            return delegate.getString();
        } catch (final IllegalStateException ise) {
            throw new IllegalArgumentException(ise);
        }
    }

    @Override
    public JsonParser skipChildren() {
        if (current == JsonToken.START_ARRAY) {
            delegate.skipArray();
            current = JsonToken.END_ARRAY;
        } else if (current == JsonToken.START_OBJECT) {
            delegate.skipObject();
            current = JsonToken.END_OBJECT;
        }
        return this;
    }

    @Override
    public String getText() {
        return delegate.getString();
    }

    @Override
    public byte getByteValue() {
        return (byte) delegate.getInt();
    }

    @Override
    public short getShortValue() {
        return (short) delegate.getInt();
    }

    @Override
    public int getIntValue() {
        return delegate.getInt();
    }

    @Override
    public float getFloatValue() {
        return (float) JsonNumber.class.cast(delegate.getValue()).doubleValue();
    }

    @Override
    public long getLongValue() {
        return delegate.getLong();
    }

    @Override
    public double getDoubleValue() {
        return JsonNumber.class.cast(delegate.getValue()).doubleValue();
    }

    @Override
    public BigInteger getBigIntegerValue() {
        return JsonNumber.class.cast(delegate.getValue()).bigIntegerValue();
    }

    @Override
    public BigDecimal getDecimalValue() {
        return delegate.getBigDecimal();
    }

    private JsonToken mapToken(final javax.json.stream.JsonParser.Event current) {
        switch (current) {
            case START_ARRAY:
                return JsonToken.START_ARRAY;
            case START_OBJECT:
                return JsonToken.START_OBJECT;
            case KEY_NAME:
                return JsonToken.FIELD_NAME;
            case VALUE_STRING:
                return JsonToken.VALUE_STRING;
            case VALUE_NUMBER:
                if (delegate.isIntegralNumber()) {
                    return JsonToken.VALUE_NUMBER_INT;
                }
                return JsonToken.VALUE_NUMBER_FLOAT;
            case VALUE_TRUE:
                return JsonToken.VALUE_TRUE;
            case VALUE_FALSE:
                return JsonToken.VALUE_FALSE;
            case VALUE_NULL:
                return JsonToken.VALUE_NULL;
            case END_OBJECT:
                return JsonToken.END_OBJECT;
            case END_ARRAY:
                return JsonToken.END_ARRAY;
            default:
                throw new IllegalArgumentException("unknown event: " + current);
        }
    }
}

The JsonGenerator bridge

The generator implementation is a bit more complicated because of three elements:

  1. The prettyfication relies on a lazy initialization to ensure enablePrettyPrint() was called (or not) before writing anything,
  2. The write method in an object - taking a key - are split in writeFieldName(key) and write(value) whereas in JSON-P API you directly call write(key, value),
  3. Because of last point you don't know if you are writing in an array or object from within a method.

In other words it implies:

  1. Each method potentially called first must check the generator already exists or not, and if not create it,
  2. The writeFieldName method must store the key name in a stack - since you can write arrays in objects - to be able to reuse it in the write value method and pass it properly to JSON-P,
  3. Each writeStart method - for object or array - must track that state in a stack too to know if we must rely on a key or not for current value write call.

Concretely, here is a potential implementation:

class JsonGeneratorImpl extends JsonGenerator {
    private final JsonFactory factory;
    private final javax.json.stream.JsonGenerator providedGenerator;
    private final Function<Boolean, javax.json.stream.JsonGenerator> generatorFactory;

    private javax.json.stream.JsonGenerator delegate;
    private boolean pretty;

    // to manage the diff between write(key, value) and writeKey()writeValue() APIs
    private final LinkedList<String> keys = new LinkedList<>();
    private final LinkedList<Boolean> containerIsArray = new LinkedList<>();

    JsonGeneratorImpl(final JsonFactory factory,
                      final Function<Boolean, javax.json.stream.JsonGenerator> generatorFactory) {
        this.factory = factory;
        this.generatorFactory = generatorFactory;
        this.providedGenerator = delegate;
    }

    @Override
    public void enablePrettyPrint() throws IOException {
        pretty = true;
    }

    @Override
    public JsonFactory getFactory() {
        return factory;
    }

    @Override
    public void flush() {
        delegate.flush();
    }

    @Override
    public void close() {
        delegate.close();
    }

    @Override
    public void writeStartArray() {
        if (containerIsArray.isEmpty()) {
            ensureGenerator();
            delegate.writeStartArray();
        } else if (containerIsArray.getLast()) {
            delegate.writeStartArray();
        } else {
            delegate.writeStartArray(keys.removeLast());
        }
        containerIsArray.add(true);
    }

    @Override
    public void writeEndArray() {
        delegate.writeEnd();
        containerIsArray.removeLast();
    }

    @Override
    public void writeStartObject() {
        if (containerIsArray.isEmpty()) {
            ensureGenerator();
            delegate.writeStartObject();
        } else if (containerIsArray.getLast()) {
            delegate.writeStartObject();
        } else {
            delegate.writeStartObject(keys.removeLast());
        }
        containerIsArray.add(false);
    }

    @Override
    public void writeEndObject() {
        delegate.writeEnd();
        containerIsArray.removeLast();
    }

    @Override
    public void writeFieldName(final String name) {
        keys.add(name);
    }

    @Override
    public void writeNull() {
        if (containerIsArray.isEmpty()) {
            ensureGenerator();
            delegate.writeNull();
        } else if (containerIsArray.getLast()) {
            delegate.writeNull();
        } else {
            delegate.writeNull(keys.removeLast());
        }
    }

    @Override
    public void writeString(final String value) {
        if (containerIsArray.isEmpty()) {
            ensureGenerator();
            delegate.write(value);
        } else if (containerIsArray.getLast()) {
            delegate.write(value);
        } else {
            delegate.write(keys.removeLast(), value);
        }
    }

    @Override
    public void writeBoolean(final boolean state) {
        if (containerIsArray.isEmpty()) {
            ensureGenerator();
            delegate.write(state);
        } else if (containerIsArray.getLast()) {
            delegate.write(state);
        } else {
            delegate.write(keys.removeLast(), state);
        }
    }

    @Override
    public void writeNumber(final int v) {
        if (containerIsArray.isEmpty()) {
            ensureGenerator();
            delegate.write(v);
        } else if (containerIsArray.getLast()) {
            delegate.write(v);
        } else {
            delegate.write(keys.removeLast(), v);
        }
    }

    @Override
    public void writeNumber(final long v) {
        if (containerIsArray.isEmpty()) {
            ensureGenerator();
            delegate.write(v);
        } else if (containerIsArray.getLast()) {
            delegate.write(v);
        } else {
            delegate.write(keys.removeLast(), v);
        }
    }

    @Override
    public void writeNumber(final BigInteger v) {
        if (containerIsArray.isEmpty()) {
            ensureGenerator();
            delegate.write(v);
        } else if (containerIsArray.getLast()) {
            delegate.write(v);
        } else {
            delegate.write(keys.removeLast(), v);
        }
    }

    @Override
    public void writeNumber(final float v) {
        if (containerIsArray.isEmpty()) {
            ensureGenerator();
            delegate.write(v);
        } else if (containerIsArray.getLast()) {
            delegate.write(v);
        } else {
            delegate.write(keys.removeLast(), v);
        }
    }

    @Override
    public void writeNumber(final double v) {
        if (containerIsArray.isEmpty()) {
            ensureGenerator();
            delegate.write(v);
        } else if (containerIsArray.getLast()) {
            delegate.write(v);
        } else {
            delegate.write(keys.removeLast(), v);
        }
    }

    @Override
    public void writeNumber(final BigDecimal v) {
        if (containerIsArray.isEmpty()) {
            ensureGenerator();
            delegate.write(v);
        } else if (containerIsArray.getLast()) {
            delegate.write(v);
        } else {
            delegate.write(keys.removeLast(), v);
        }
    }

    @Override
    public void writeNumber(final String encodedValue) {
        if (containerIsArray.isEmpty()) {
            ensureGenerator();
            delegate.write(encodedValue);
        } else if (containerIsArray.getLast()) {
            delegate.write(encodedValue);
        } else {
            delegate.write(keys.removeLast(), encodedValue);
        }
    }

    private void ensureGenerator() {
        delegate = generatorFactory.apply(pretty);
    }
}

 

Use our JsonpJsonFactory

Now we have a complete implementation we just need to pass it as parameter in our Google SDK usage. For instance for google analytics client it will look like that - but most of the SDK has the same kind of methods so it is easy to generalize:

final JsonFactory jsonFactory = JsonpJsonFactory.of(); // <1>

final GoogleCredential credential = GoogleCredential.fromStream(
  stream, httpTransport,
  jsonFactory); // <2>

final AnalyticsReporting reporting = new AnalyticsReporting.Builder(
    httpTransport,
    jsonFactory, // <3>
    credential)
  .setApplicationName("my-app")
  .build();
  1. We create an instance of our factory,

  2. When reading our credentials we pass our factory instead of letting Google SDK default to default global JacksonFactory,

  3. When creating a service we pass to its builder our JsonFactory instance.

With that code Jackson is not even loaded!

If you are in a hurry use google-jsonp

If you don't want to create this glue code yourself I already released it on central and you can just add to your dependencies the following one:

<dependency>
  <groupId>com.github.rmannibucau</groupId>
  <artifactId>google-jsonp</artifactId>
  <version>1.0</version>
</dependency>

 

From the same author:

In the same category: