Skip to content

Add aliases to JsonValue to enum value to be decoded from different JSON values #1459

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

Open
wants to merge 16 commits into
base: master
Choose a base branch
from

Conversation

mateusfccp
Copy link

@mateusfccp mateusfccp commented Nov 26, 2024

Solves #1380.

Adds a parameter to JsonValue called aliases, which accepts a list of values that can be alternatively used to decode a JSON.

Example:

enum StatusCode {
  @JsonValue(200, aliases: [201, 202])
  success,
  @JsonValue(301)
  movedPermanently,
  @JsonValue(302)
  found,
  @JsonValue(500)
  internalServerError,
}

With the code above, all of 200, 201 and 202 would be decoded as StatusCode.success.

When calling toJson, however, the value parameter is used. In this case, 200.

Notes

  • The $enumDecode and $enumDecodeNullable were not changed because they are public and there are packages that depend on them. Instead, $enumDecodeWithDecodeMap and $enumDecodeNullableWithDecodeMap were introduced. We may want to deprecate the former at some point and remove them in a major version.

@kevmoo
Copy link
Collaborator

kevmoo commented Dec 2, 2024

Ideally we'd have a couple of tests to validate the failure cases, but I get that those can be tricky to add.

@mateusfccp
Copy link
Author

@kevmoo What would be the failure cases for this PR? Can you give an example?

@kevmoo
Copy link
Collaborator

kevmoo commented Dec 2, 2024

@kevmoo What would be the failure cases for this PR? Can you give an example?

Unsupported types in the set is the big one.
null isn't supported, right?
Those types of things.

@mateusfccp
Copy link
Author

mateusfccp commented Dec 2, 2024

@kevmoo What would be the failure cases for this PR? Can you give an example?

Unsupported types in the set is the big one. null isn't supported, right? Those types of things.

Ah, yes, the idea is to mimic value, so until we have proper union types in Dart, we use Object? but should only accept String and int.

I am going to work on this test.

@kevmoo
Copy link
Collaborator

kevmoo commented Dec 17, 2024

You'll need to rebase this fix.

@mateusfccp
Copy link
Author

You'll need to rebase this fix.

I rebased to master and included the requested test.

In my local machine, a test was failing, but I didn't change anything related to it (the *ToJson generator).

image

@mateusfccp
Copy link
Author

Mmm, I had made a regular merge, but GitHub is saying to me that "this branch must not contain merge commits", so I rewrote the commits using a rebase instead, but it is still giving me this message.

@mateusfccp
Copy link
Author

mateusfccp commented Dec 18, 2024

Well, the test that did not pass locally passed here, so we are good with it.

@mateusfccp
Copy link
Author

mateusfccp commented May 23, 2025

@kevmoo Is there something preventing the code review from proceeding?

Copy link
Collaborator

@kevmoo kevmoo left a comment

Choose a reason for hiding this comment

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

I don't like the repeated map EVERYWHERE for one-off feature.

Could you noodle on that a bit?

@@ -31,21 +34,45 @@ bool? enumFieldWithNullInEncodeMap(DartType targetType) {
return enumMap.values.contains(null);
}

String? enumValueMapFromType(
String? enumMapsFromType(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Undo this rename. Makes reviewing more tricky.

Copy link
Author

Choose a reason for hiding this comment

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

Done.

const _$GreekEnumMap = {
Greek.alpha: 'alpha',
Greek.beta: 'beta',
Greek.gamma: 'gamma',
Greek.delta: 'delta',
};

// ignore: unused_element
const _$GreekEnumDecodeMap = {
Copy link
Collaborator

Choose a reason for hiding this comment

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

So we're changing the generated code and creating a NEW map, even if the dev doesn't use the new feature?

Copy link
Author

Choose a reason for hiding this comment

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

@kevmoo Yes, basically.

The old map is left as is. However, in the current code, the map is used both for encoding and decoding. Decoding is done by linearly checking if an entry has the expected value for that key. It works similar to a Bimap.

By using two maps, we have O(1) access to the expected decoded value. But this is only a consequence of this approach, and I don't even think it will make any difference in practice unless someone decodes an outstanding quantity of Json values.

The main point is that if we introduce aliases, we have an asymmetric relationship between the two sets (the sets of encoded and decoded values). We want encoding to have a single resulting value (it is the one defined by the first parameter of the JsonValue constructor), but it can be decoded by more than one value (which is this same value + the ones declared in the alias parameter).

Because of this, we can't work with a single map anymore unless:

  • We revert the map and key values, so we avoid duplicated keys.
  • We guarantee that the first value is always the one that is expected when encoding.

Another approach could be to use two maps, the one we already have and an optional map aliases that will only be generated if aliases are used. Then, in the $enumDecodeWithDecodeMap function, we accept an optional aliases map as parameter, which, if passed, will first check for the entries like it is doing today and, if not found, it will try to find it in the passed aliases map (if any), falling back to a runtime error as it is today.

This could work, but I don't think it is a better approach than what I chose to use on this PR. I just splits the map into two, which guarantees O(1) access and a clear distinction between encoding/decoding expectations.

Maybe you have a third idea about how to tackle this issue?

Copy link
Author

Choose a reason for hiding this comment

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

(Ps. the $enumDecode function is not even used anymore, I just didn't remove it entirely because I don't know if it would constitute a breaking change. Supposedly not, because we don't expose it in the public API, but I wanted to check with you before. If we are good, $enumDecodeWithDecodeMap can be renamed to $enumDecode, as it replaces its purpose completely with this approach)

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