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

json: Ignore unknown enum values #170

Merged
merged 4 commits into from
Oct 11, 2021
Merged

Conversation

whs
Copy link
Contributor

@whs whs commented Oct 10, 2021

While parsing proto from JSON, any unknown enum values would result in assertion error. This would cause backward compatibility issue.

I looked on how other implementations are doing it and found that the protobuf team has decided that ignoringUnknownFields not ignoring missing enum values is a bug. The Java implementation now returns 0 if ignoringUnknownFields is set.

@whs whs changed the title json: Parse unknown enum values as -1 json: Parse unknown enum values as 0 Oct 10, 2021
@timostamm
Copy link
Owner

Thanks for this PR, @whs.

It looks like the PR you mentioned has not landed. This PR however has.

The difference between those PRs is that the latter one ignores unrecognized values instead of setting the field to 0 or anything else. This is not relevant most of the time, because you would usually read from JSON into a blank message (that already has field value 0), but I think it is important to be precise here.

@whs
Copy link
Contributor Author

whs commented Oct 11, 2021

I suppose returning undefined for the merging algorithm to sort that out would be the more correct behavior here. I've updated the PR.

Copy link
Owner

@timostamm timostamm left a comment

Choose a reason for hiding this comment

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

Left some comments on how fields with unrecognized values could be skipped without rewriting the entire class. It's not pretty, but I'd still prefer.

Comment on lines 177 to 180
it('return -1 if unknown value was found, but ignoreUnknownFields was set', function () {
const reader = new ReflectionJsonReader({typeName: '.test.Message', fields: []});
expect(reader.enum([".spec.SimpleEnum", SimpleEnum], 'test', '', true)).toEqual(0);
})
Copy link
Owner

Choose a reason for hiding this comment

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

If the string value is not recognized, nothing should be done here.

@@ -196,7 +196,7 @@ export class ReflectionJsonReader {
/**
* google.protobuf.NullValue accepts only JSON `null`.
*/
enum(type: EnumInfo, json: unknown, fieldName: string): UnknownEnum {
enum(type: EnumInfo, json: unknown, fieldName: string, ignoreUnknownFields = false): UnknownEnum {
Copy link
Owner

Choose a reason for hiding this comment

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

Can you omit the default argument? ignoreUnknownStringValue: boolean is enough.

Copy link
Owner

Choose a reason for hiding this comment

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

For unrecognized string values, we should return false. The return type of the method can be UnknownEnum | false. The method needs a doc explaining that false means an unknown string value was encountered.

Comment on lines 117 to 119
case "enum":
val = this.enum(field.V.T(), jsonObjValue, field.name);
val = this.enum(field.V.T(), jsonObjValue, field.name, options.ignoreUnknownFields);
break;
Copy link
Owner

Choose a reason for hiding this comment

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

Unfortunately, the existing code isn't suited well to skipping fields here. We'll have to resort to labels 🙄

                        case "enum":
                            val = this.enum(field.V.T(), jsonObjValue, field.name, options.ignoreUnknownFields);
                            if (val === false)
                                break map_entries;
                            break;

And further above:

// read entries
map_entries:
for (const [jsonObjKey, jsonObjValue] of Object.entries(jsonValue)) {

Copy link
Contributor Author

@whs whs Oct 11, 2021

Choose a reason for hiding this comment

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

Shouldn't this be continue instead of break? (eg. {key1: "INVALID_ENUM", key2: "VALID_ENUM"} would break skip the second key?)

Copy link
Owner

Choose a reason for hiding this comment

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

Sorry, you're right, let's not break the entire loop!

@@ -154,7 +154,7 @@ export class ReflectionJsonReader {
break;

case "enum":
val = this.enum(field.T(), jsonItem, field.name);
val = this.enum(field.T(), jsonItem, field.name, options.ignoreUnknownFields);
Copy link
Owner

Choose a reason for hiding this comment

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

Same as for maps, we'll need a label array_entries and break that here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For array if we continue I suppose we would cause the output length to be less than the input length?

Copy link
Owner

Choose a reason for hiding this comment

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

Yes, same for map. That's what JsonFormat.java does.

@@ -180,7 +180,7 @@ export class ReflectionJsonReader {
break;

case "enum":
target[localName] = this.enum(field.T(), jsonValue, field.name);
target[localName] = this.enum(field.T(), jsonValue, field.name, options.ignoreUnknownFields);
Copy link
Owner

Choose a reason for hiding this comment

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

This one will work just by not setting the value:

                    case "enum":
                        let val = this.enum(field.T(), jsonValue, field.name, options.ignoreUnknownFields);
                        if (val === false)
                            break;
                        target[localName] = val;
                        break;

@timostamm
Copy link
Owner

I suppose returning undefined for the merging algorithm to sort that out would be the more correct behavior here. I've updated the PR.

Thanks, just saw that while reviewing 😅 . Whether we return false or undefined does not matter to me, but the read method cannot set undefined values - this would break its contract, it really has to skip.

@whs
Copy link
Contributor Author

whs commented Oct 11, 2021

Updated the PR to skip fields - also added additional tests on read()

Copy link
Owner

@timostamm timostamm left a comment

Choose a reason for hiding this comment

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

Thank you 🙏

@timostamm timostamm changed the title json: Parse unknown enum values as 0 json: Ignore unknown enum values Oct 11, 2021
@timostamm timostamm merged commit 995728c into timostamm:master Oct 11, 2021
@timostamm
Copy link
Owner

Released in v2.0.7

@whs
Copy link
Contributor Author

whs commented Oct 12, 2021

Would you mind labeling this PR with hacktoberfest-accepted. It's a new rule this year.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants