Skip to content

centralize the serialization/deserialization logic #36

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Oct 26, 2020

Conversation

niemannd
Copy link
Contributor

This PR provides a centralized json handling system.
The Idea is to use the same JsonHandler throughout the whole sdk, centralize the logic to serialize/deserialize and make it possible to change the underlying Json library without breaking changes.
This is the json pendant to #34

ATM the Code is not integrated anywhere. this would be topic for a future PR.

Usage would be

Movie movie = handler.decode("{"id":"1"}", Movie.class)
List<Movie> movieList= handler.decode("[{"id":"1"}{"id":"2"}{"id":"3"}]", Arraylist.class, Movie.class)

@eskombro
Copy link
Member

eskombro commented Oct 14, 2020

I really like this 'abstract' vision that you are implementing, because to me it seems it can help with reducing strong dependencies and adding some possibilities of customization. But in this particular case, I was just wondering: does it happen often that you want to swap from one Json library to another one? I'm not against it at all, I'm just asking because I think this introduces more complexity in the code and want to be sure it is indeed useful. I am used to developing in languages where you have either a native way of handling JSON, or a library that kind of is the main choice for everyone, but I don't know how Java developers handle this in general, I see there are a few options that are used widely and don't seem to have a real consensus.

In the other hand, centralizing json handling seems like an amazing idea for usability and readability! 👍

Thanks @niemannd

@niemannd
Copy link
Contributor Author

I [...] want to be sure it is indeed useful.

In the Java Ecosystem you have different userclusters that use different libraries.
The JakartaEE people use http://json-b.net/ and https://javaee.github.io/jsonp/
The Spring People use https://github.com/FasterXML/jackson
Some people like Gson, some use https://github.com/fangyidong/json-simple

To not scare away a possible userbase i wrote this little abstraction to allow them to integrate an existing json library. In some environments "just pull in a second json lib" doesn't fly and will get you in trouble. I hope its not that big of a braintwister 😄

@eskombro
Copy link
Member

eskombro commented Oct 14, 2020

To not scare away a possible userbase i wrote this little abstraction to allow them to integrate an existing json library [...]

Thanks a lot! Your answer is quite reassuring, and it seems to be exactly what we want!!!

Then again, I ask myself ( and to you in here :) ) the same question than for other abstactions... should we make a choice to use a specific library by default, to make it easy to use out-of-the-box? With an abstraction like this one you are introducing, this kind of choice is a big help instead of a constraint! That's great!

I see you used gson, maybe because it was already used in this SDK, maybe because is your choice. But feel free to use a different one if you prefer

@niemannd
Copy link
Contributor Author

I see you used gson, maybe because it was already used in this SDK, maybe because is your choice. But feel free to use a different one if you prefer

I added two additional Handlers.

JacksonJsonHandler that uses Jackson
JsonBJsonHandler that uses the JSON-B API with eclipse yasson as an implementation for the tests.
I never worked with JSON-B and yasson before, so use with caution. This handler is probably not bug free 😄

should we make a choice to use a specific library by default, to make it easy to use out-of-the-box?

Maybe we can automatically decide what implementation we use based on the classpath.
If Jackson is present, use JacksonJsonHandler. If Gson is present, use Gson.
Could look something like this:

Client
    .newBuilder()
    .withConfig(...)
    .withAutodetectHttpClient()  // for autodetect
    .withHttpClient(new ApacheHttpClient(...)) // for a specific implementation
    .withAutodetectJsonHandler()  // for autodetect
    .withJsonHandler(new JacksonJsonHandler(...))  // for a specific implementation
    .build();

@niemannd
Copy link
Contributor Author

thats probably because the dependencies JacksonJsonHandler and JsonbJsonHandler need are compileOnly and not bundled with the dependency.

To use them your need the following dependencies in your build.gradle
For JacksonJsonHandler
compile group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.11.3'
For JsonbJsonHandler
compile group: 'javax.json.bind', name: 'javax.json.bind-api', version: '1.0' and an implementation like eclipse yasson.
(in JavaEE/JakartaEE implementation should be provided by the application server, so only the api would be required)

@eskombro
Copy link
Member

eskombro commented Oct 26, 2020

Oh sorry, I deleted the comment as I realized that! Thanks for your help!

Works smoothly 👍

@Test
void serialize() throws Exception {
assertEquals("test", classToTest.encode("test"));
when(mapper.toJson(any(Movie.class))).thenThrow(new JsonbException("Oh boy!"));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😂

eskombro
eskombro previously approved these changes Oct 26, 2020
Copy link
Member

@eskombro eskombro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, great! 🌮

@eskombro
Copy link
Member

Ready to merge @niemannd ?

@niemannd
Copy link
Contributor Author

Ready to merge

@eskombro
Copy link
Member

eskombro commented Oct 26, 2020

Another question, sorry...

Assuming

import com.meilisearch.sdk.Client;
import com.meilisearch.sdk.Config;
import com.meilisearch.sdk.UpdateStatus;
import com.meilisearch.sdk.Index;
import com.meilisearch.sdk.json.GsonJsonHandler;
import com.meilisearch.sdk.json.JacksonJsonHandler;
import com.meilisearch.sdk.json.JsonbJsonHandler;

final class Document {
  String id;
  String title;
}

final class Results {
  Document[] hits;
}

public class TestJava {
  public static void main(String[] args) throws Exception {

    GsonJsonHandler json_gson = new GsonJsonHandler();
    JacksonJsonHandler json_jackson = new JacksonJsonHandler();
    JsonbJsonHandler json_jsonb = new JsonbJsonHandler();

    final String indexUid = "books";
    final String documents = "["
      + "{\"id\": 123, \"title\": \"Pride and Prejudice\"},"
      + "{\"id\": 456, \"title\": \"Le Petit Prince\"},"
      + "{\"id\": 1, \"title\": \"Alice In Wonderland\"},"
      + "{\"id\": 1344, \"title\": \"The Hobbit\"},"
      + "{\"id\": 4, \"title\": \"Harry Potter and the Half-Blood Prince\"},"
      + "{\"id\": 2, \"title\": \"The Hitchhiker\'s Guide to the Galaxy\"}"
      + "]";

    Client client = new Client(new Config("http://localhost:7700", "masterKey"));
    
    Index index = client.createIndex(indexUid);
    UpdateStatus update = json_gson.decode(
      index.addDocuments(documents),
      UpdateStatus.class
    );

    index.waitForPendingUpdate(update.getUpdateId());

    // .... HERE GOES THE TEST
    // 
    // 
    // .... HERE GOES THE TEST

    client.deleteIndex(indexUid);

  }
}

If I test

Results res_gson = json_gson.decode(index.search("a"), Results.class);
System.out.println(res_gson.hits);
System.out.println(json_gson.encode(res_gson.hits));

I get the output:

[LtestJava.Document;@4034c28c
[{"id":"1","title":"Alice In Wonderland"},{"id":"123","title":"Pride and Prejudice"},{"id":"4","title":"Harry Potter and the Half-Blood Prince"}]

Then I try:

Results res_jackson = json_jackson.decode(index.search("a"), Results.class);
System.out.println(res_jackson.hits);

and the output is:

null

This seems to be a problem, can you check please?

@niemannd
Copy link
Contributor Author

Thats a problem with the way you declared those classes.
Jackson can't serialize/deserialize package-private and private fields with default settings. Either make them public or (recommended) use Getters/Setters.

package com.meilisearch;

import com.meilisearch.sdk.Client;
import com.meilisearch.sdk.Config;
import com.meilisearch.sdk.Index;
import com.meilisearch.sdk.UpdateStatus;
import com.meilisearch.sdk.json.GsonJsonHandler;
import com.meilisearch.sdk.json.JacksonJsonHandler;
import com.meilisearch.sdk.json.JsonbJsonHandler;
import org.junit.jupiter.api.Test;

public class TestJava {

	@Test
	void name() throws Exception {

		GsonJsonHandler json_gson = new GsonJsonHandler();
		JacksonJsonHandler json_jackson = new JacksonJsonHandler();
		JsonbJsonHandler json_jsonb = new JsonbJsonHandler();

		final String indexUid = "books";
		final String documents = "["
			+ "{\"id\": 123, \"title\": \"Pride and Prejudice\"},"
			+ "{\"id\": 456, \"title\": \"Le Petit Prince\"},"
			+ "{\"id\": 1, \"title\": \"Alice In Wonderland\"},"
			+ "{\"id\": 1344, \"title\": \"The Hobbit\"},"
			+ "{\"id\": 4, \"title\": \"Harry Potter and the Half-Blood Prince\"},"
			+ "{\"id\": 2, \"title\": \"The Hitchhiker\'s Guide to the Galaxy\"}"
			+ "]";

		Client client = new Client(new Config("http://localhost:7700", "masterKey"));

		Index index = client.createIndex(indexUid);
		UpdateStatus update = json_gson.decode(
			index.addDocuments(documents),
			UpdateStatus.class
		);

		index.waitForPendingUpdate(update.getUpdateId());

		Results res_gson = json_gson.decode(index.search("a"), Results.class);
		System.out.println(res_gson.hits);
		System.out.println(json_gson.encode(res_gson.hits));

		Results res_jackson = json_jackson.decode(index.search("a"), Results.class);
		System.out.println(res_jackson.hits);
		System.out.println(json_jackson.encode(res_jackson.hits));

		client.deleteIndex(indexUid);
	}

	public static class Document {
		private String id;
		private String title;

		public String getId() {
			return id;
		}

		public Document setId(String id) {
			this.id = id;
			return this;
		}

		public String getTitle() {
			return title;
		}

		public Document setTitle(String title) {
			this.title = title;
			return this;
		}
	}

	public static class Results {
		private Document[] hits;

		public Document[] getHits() {
			return hits;
		}

		public Results setHits(Document[] hits) {
			this.hits = hits;
			return this;
		}
	}
}

@eskombro
Copy link
Member

Great, sorry, as usual when you try to do things quickly you end up working double haha!

I'd like to add an integration test for this, using the 3 json implementations, and adding an assertion to check that the hits are the same no matter what encoder/decoder you use. Could you add it to the PR? I can commit a test if you prefer< but I prefer that everythiong is tested to avoid future problems :)

@niemannd
Copy link
Contributor Author

I added unit tests for the other two JsonHandler.
At the moment i cant add an integration test, because the code is not really used.
In the future i would use something like this for integration tests.
Runs every encode/decode on all Handlers and compares the result. (probably still buggy)

public class MultiJsonHandler implements JsonHandler {
	private final JacksonJsonHandler jacksonJsonHandler = new JacksonJsonHandler();
	private final JsonbJsonHandler jsonbJsonHandler = new JsonbJsonHandler();
	private final GsonJsonHandler gsonJsonHandler = new GsonJsonHandler();

	@Override
	public String encode(Object o) throws Exception {
		final String encodeJackson = jacksonJsonHandler.encode(o);
		final String encodeJsonb = jsonbJsonHandler.encode(o);
		final String encodeGson = gsonJsonHandler.encode(o);

		assertThat(encodeGson, equalTo(encodeJackson));
		assertThat(encodeGson, equalTo(encodeJsonb));
		return encodeGson;
	}

	@Override
	public <T> T decode(Object o, Class<?> targetClass, Class<?>... parameters) throws Exception {
		T decodeJackson = jacksonJsonHandler.decode(o, targetClass, parameters);
		T decodeJsonb = jsonbJsonHandler.decode(o, targetClass, parameters)
		T decodeGson = gsonJsonHandler.decode(o, targetClass, parameters);
		assertThat(decodeGson, equalTo(decodeJackson));
		assertThat(decodeGson, equalTo(decodeJsonb));
		return decodeGson;
	}
}

@eskombro
Copy link
Member

Amazing @niemannd 🚀 🚀 🚀 🚀

@eskombro eskombro merged commit e1bba35 into meilisearch:master Oct 26, 2020
@curquiza curquiza mentioned this pull request Nov 25, 2020
@niemannd niemannd deleted the feat/json-handling branch February 5, 2021 18:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants