Skip to content
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

Roundtrip of unrecognised JSON properties #1394

Merged
merged 16 commits into from
Apr 26, 2022
Merged

Conversation

VysotskiVadim
Copy link
Contributor

@VysotskiVadim VysotskiVadim commented Apr 4, 2022

Goal

Implementation of #1344
Models from mapbox-java should not erase unrecognised json properties during fromJson -> toJson

example

If we have a model with a single property a

public abstract Model {
    public abstract String a();
}

and a JSON with properties a and b

{"a":"value", "b":"value"}

the model should let unrecognised fields to do a roundtrip

String json = {"a":"value", "b":"value"}
assertEquals(json, Model.fromJson(json).toJson());

Implementation

I forked the library which generates gson type adapters for us. Now if there are some unrecognised fields a type adapter put them into unrecognised fields map.
I will share a link when fork will be publicly available.

@VysotskiVadim VysotskiVadim requested review from RingerJK, LukasPaczos and a team April 4, 2022 09:21
DirectionsResponse responseFromJson1 = DirectionsResponse.fromJson(json);
public void testToFromJsonWithRealResponse() throws Exception {
Gson gson = new GsonBuilder().create();
String originalJson = loadJsonFixture(DIRECTIONS_V5_PRECISION6_FIXTURE_ARTIFICIAL_FIELDS);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added DIRECTIONS_V5_PRECISION6_FIXTURE_ARTIFICIAL_FIELDS just because DirectionsResponse always adds routeIndex and requestUuid.

@VysotskiVadim
Copy link
Contributor Author

VysotskiVadim commented Apr 7, 2022

Did rough testing of performance. No microbenchmarks was involved, but I guess we can less or more understand performance impact.

Test 1

Deserialised and serialised directions repose 1000 times:

   for (int i = 0; i < 1000; i++) {
      DirectionsResponse response = DirectionsResponse.fromJson(regularJsonString);
      String json = response.toJson();
    }

2210 ms

Test 2

Deserialised and serialised mutated directions repose 1000 times:

   for (int i = 0; i < 1000; i++) {
      DirectionsResponse response = DirectionsResponse.fromJson(mutatedJsonString);
      String json = response.toJson();
    }

3075 ms
Mutated json is a directions response where to each property was added a few unrecognised fields. See https://github.com/mapbox/mapbox-java/pull/1394/files#diff-2c855af9309578033b9a51b9306ce977b4241a7b7197984bb16b83f26a49fdfbR12

Test 3

Tested mutated json serialisation and deserialisation 1000 times on main branch:

   for (int i = 0; i < 1000; i++) {
      DirectionsResponse response = DirectionsResponse.fromJson(mutatedJson);
      String back = response.toJson();
    }

2333 ms

@@ -501,5 +502,8 @@ public abstract static class Builder {
* @since 3.0.0
*/
public abstract BannerComponents build();

@Nullable
abstract Builder unrecognised(Map<String, Object> value);
Copy link
Contributor

Choose a reason for hiding this comment

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

we know that unrecognized fields are Strings, this way, let's make Map<String, String>

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it's not only strings. It can also be JsonObject, JsonArray, etc.

Copy link
Contributor

Choose a reason for hiding this comment

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

why do you want to operate with these fields as JsonObject or JsonArrays? Looks like we only need to serialize/deserialize them. So no need to keep Objects

Copy link
Member

Choose a reason for hiding this comment

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

I was actually thinking whether we could consider making this map public (no in builder but in the resulting type), so that if someone would like to access the unrecognized fields, they don't need to serialize the type first. I believe this will be needed soon (for example for experimental EV properties).

Copy link
Contributor

Choose a reason for hiding this comment

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

@LukasPaczos Experimental EV and other options are allocated in RouteOptions. We don't extend RouteOptions with Unrecognized fields I think 🤔 Suppose, it's separate initiative, not really connected with this PR

Copy link
Member

Choose a reason for hiding this comment

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

Yea, as discussed internally - I think we should follow the same logic in both places.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

RouteOptions extends DirectionsJsonObject so it already has unrecognised fields


/**
* Provides a base class for Directions model classes.
*
* @since 3.4.0
*/
public class DirectionsJsonObject implements Serializable {
public abstract class DirectionsJsonObject implements Serializable {
Copy link
Member

Choose a reason for hiding this comment

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

This is a breaking change (not sure how impactful it would actually be in practice). Could we make the methods return empty and implement downstream, how would it look like?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

nice catch 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

AutoValue won't generate fields for not abstract methods

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I see a few possible solutions right now.

Do breaking change

Maybe we can make ourself believe that it's not really a breaking change using following arguments: it more like a fix of a bug, nobody should have created an instance of DirectionsJsonObject, it doesn't make any sense to create an instance of DirectionsJsonObject. But abstract do really changes API.

Workaround

I can create an abstract class BaseDirectionsJsonObject which is going to be a parent of DirectionsJsonObject and move unrecognised filed there. I find this solution ugly but it won't break API.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think that we need to decide if somebody could be affected by not being able to instantiate DirectionsJsonObject. If we're sure that we will affect nobody, and even if we will we're ready to reply in their issues on github, then I let's break the rule.
If we don't feel confident, let's workaround

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I feel confident. I can't imaging a case when somebody instantiate DirectionsJsonObject but if somebody does it, I'd like to learn their case when they open an issue on our github 🙂
@LukasPaczos , @RingerJK , what do you think about it?

Copy link
Member

Choose a reason for hiding this comment

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

@VysotskiVadim I agree. There's no practical reason to extend the DirectionsJsonObject for developers, I think we can break here. Let's clearly state this in the changelog and go from there.

Comment on lines +55 to +60
JsonObject originalJsonObject = gson.fromJson(originalJson, JsonObject.class);

DirectionsResponse model = DirectionsResponse.fromJson(originalJson);
JsonObject jsonFromObject = gson.fromJson(model.toJson(), JsonObject.class);

assertEquals(originalJsonObject, jsonFromObject);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
JsonObject originalJsonObject = gson.fromJson(originalJson, JsonObject.class);
DirectionsResponse model = DirectionsResponse.fromJson(originalJson);
JsonObject jsonFromObject = gson.fromJson(model.toJson(), JsonObject.class);
assertEquals(originalJsonObject, jsonFromObject);
DirectionsResponse model = DirectionsResponse.fromJson(originalJson);
String jsonFromModel = model.toJson();
assertEquals(originalJson, jsonFromModel);

We want to ensure the resulting strings are correct, the object wrapping step seems unnecessary.

Copy link
Contributor Author

@VysotskiVadim VysotskiVadim Apr 14, 2022

Choose a reason for hiding this comment

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

Strings won't be the same. An order of fields changes when json gets serialised to java model and then gets deserialised back. And I think it's acceptable behaviour. JsonObject compares that 2 JSONs has the same set of fields with the same value and it doesn't care about order.

Copy link
Member

Choose a reason for hiding this comment

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

That makes sense 👍 I guess the equality of a serialized form is just a side effect, not a guarantee.

@codecov
Copy link

codecov bot commented Apr 22, 2022

Codecov Report

Merging #1394 (edb8750) into main (d9d5af3) will increase coverage by 0.22%.
The diff coverage is 95.74%.

Impacted file tree graph

@@             Coverage Diff              @@
##               main    #1394      +/-   ##
============================================
+ Coverage     75.23%   75.45%   +0.22%     
- Complexity      920      926       +6     
============================================
  Files           125      125              
  Lines          3957     3960       +3     
  Branches        645      646       +1     
============================================
+ Hits           2977     2988      +11     
+ Misses          706      698       -8     
  Partials        274      274              
Impacted Files Coverage Δ
...ox/api/directions/v5/DirectionsAdapterFactory.java 100.00% <ø> (ø)
...ctions/v5/models/ExperimentalWaypointMetadata.java 0.00% <0.00%> (ø)
.../com/mapbox/api/directions/v5/models/Metadata.java 0.00% <0.00%> (ø)
...onsrefresh/v1/DirectionsRefreshAdapterFactory.java 100.00% <ø> (ø)
...i/geocoding/v5/models/GeocodingAdapterFactory.java 100.00% <ø> (ø)
.../matching/v5/models/MapMatchingAdapterFactory.java 100.00% <ø> (ø)
...com/mapbox/api/matrix/v1/MatrixAdapterFactory.java 100.00% <ø> (ø)
...mization/v1/models/OptimizationAdapterFactory.java 100.00% <ø> (ø)
...rsions/models/RouteTileVersionsAdapterFactory.java 100.00% <ø> (ø)
...ava/com/mapbox/api/directions/v5/models/Admin.java 57.14% <100.00%> (ø)
... and 38 more


@Nullable
@UnrecognizedJsonProperties
abstract Map<String, SerializableJsonElement> unrecognised();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@LukasPaczos , let me open it or provide alternative API in separate PR so that we can discuss it separately.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

services-directions/build.gradle Show resolved Hide resolved
services-matrix/build.gradle Outdated Show resolved Hide resolved
Comment on lines +893 to +895
if (property.equals(ACCESS_TOKEN_URL_PARAM_NAME)) {
continue;
}
Copy link
Member

Choose a reason for hiding this comment

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

Do we have a test that covers this already?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yep, I added this line because one of tests failed

@VysotskiVadim
Copy link
Contributor Author

@RingerJK , @LukasPaczos , please review companion PR mapbox/auto-value-gson#2

@VysotskiVadim VysotskiVadim marked this pull request as ready for review April 26, 2022 11:50
Copy link
Member

@LukasPaczos LukasPaczos left a comment

Choose a reason for hiding this comment

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

LGTM, nice work!

@VysotskiVadim VysotskiVadim changed the title Round trip of unrecognised JSON properties Roundtrip of unrecognised JSON properties Apr 26, 2022
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.

3 participants