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

WIP: Add json (serde) support #1081

Draft
wants to merge 30 commits into
base: master
Choose a base branch
from
Draft

WIP: Add json (serde) support #1081

wants to merge 30 commits into from

Conversation

cynecx
Copy link

@cynecx cynecx commented Jun 13, 2024

I started this work because I'm also working on adding Connect RPC support to tonic. So having proper protobuf json support is the first step into achieving that goal.

This (unfortunately very big) PR adds json/serde support to prost. This is still a WIP effort but it is almost feature complete and passes all conformance tests except tests related to fieldmasks.

A non-exhaustive list of TODOs:

  • Decide whether the whole "custom serde" design is the one we want to persue
  • Properly document code and public api
  • Properly put this all behind a feature flag
  • Probably split things up because this PR also includes a new Any implementation
  • ...

So I am just putting this up here to get a general vibe check...

Copy link
Collaborator

@caspermeijn caspermeijn left a comment

Choose a reason for hiding this comment

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

This is impressive work and this feature is on the project roadmap (#624). However this PR is way too big to review. I did scroll through the code and ask some questions that came to mind.

Can you split this work into smaller PRs?

tests/src/lib.rs Outdated Show resolved Hide resolved
pub trait Sealed {}
}

pub trait AnyValue: CoreAny + Message + private::Sealed {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you explain what any v2 is and why it is better?

Copy link
Author

@cynecx cynecx Jun 14, 2024

Choose a reason for hiding this comment

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

This basically tries to address the "improved Any" bulletpoint in #624. The new Any implementation provides support for JSON and "cross" serialization (JSON <-> Binary).

The summary for that change is basically going from:

struct OldAny {
    type_path: String,
    data: Vec<u8>,
}

to

struct NewAny {
    type_path: String,
    kind: enum {
        Protobuf(Vec<u8>),
        Json(JsonValue),
        Dyn(Box<dyn AnyValue>),
    }
}

prost-build/src/code_generator.rs Outdated Show resolved Hide resolved
@cynecx
Copy link
Author

cynecx commented Jun 14, 2024

However this PR is way too big to review.
Can you split this work into smaller PRs?

Yeah, it's actually on the TODO list. I just still haven't gotten to actually complete this as a whole yet.

@@ -172,6 +173,32 @@ impl Field {
_ => None,
}
}

pub fn json(&self) -> Option<Option<&Json>> {
Copy link

Choose a reason for hiding this comment

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

Wouldn't it make more sense to flatten this into Option<&Json>

Copy link
Author

@cynecx cynecx Jun 21, 2024

Choose a reason for hiding this comment

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

Conceptionally yes, but this is an actual tri-state kind of thing, because (iirc) we are skipping code generation for Self::OneOf fields. But we could also make this more flat and just add another check for an oneof field at the callsite 🤷‍♂️.

@@ -107,7 +111,7 @@ impl fmt::Display for Duration {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut d = *self;
d.normalize();
if self.seconds < 0 && self.nanos < 0 {
if self.seconds < 0 && self.nanos <= 0 || self.seconds <= 0 && self.nanos < 0 {
Copy link

Choose a reason for hiding this comment

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

This seems like an unrelated fix that can ship on its own.

@cblichmann
Copy link

Any news on this? This would be incredibly useful.

@cynecx
Copy link
Author

cynecx commented Nov 22, 2024

I've done a quick rebase and made few adjustments to pass the conformance tests again. It also seems like I need to do some tweaks to fix the CI for no-std.

Any news on this? This would be incredibly useful.

I am still way too busy with other things. What this PR needs is a split and a bit of slight documentation work. I will probably get to that at the end of December perhaps. Thanks for your patience!

@@ -14,7 +14,8 @@ proc-macro = true

[dependencies]
anyhow = "1.0.1"
itertools = ">=0.10.1, <=0.13"
heck = "0.4.1"
Copy link

@ehiggs ehiggs Nov 25, 2024

Choose a reason for hiding this comment

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

0.5 was released 9 months ago. Any reason to use a version from 2 years ago?

Copy link
Author

Choose a reason for hiding this comment

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

Not sure anymore but I guess this PR or at least my first local impl predated this commit 2b6140c.

But yeh, we can certainly bump this.

@@ -69,6 +69,10 @@ impl Duration {
result.normalize();
result
}

pub fn is_valid(&self) -> bool {
self.seconds >= -315_576_000_000 && self.seconds <= 315_576_000_000
Copy link

Choose a reason for hiding this comment

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

Sorry if I'm a tourist asking silly questions here, but what does it mean to be a duration consisting of a negative number of seconds?

Copy link

Choose a reason for hiding this comment

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

aiui, Chrono calls it a TimeDelta for this reason

Copy link
Author

Choose a reason for hiding this comment

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

There was a conformance test that checked this behavior... It's basically an api contract / invariant, that is mentioned here: https://github.com/protocolbuffers/protobuf/blob/2ad21af8406316d2b170230f93db7d10c36108dd/src/google/protobuf/duration.proto#L103.

}
}

// FIXME: Make `T` contravariant, not covariant, by changing the `T` in `PhantomData` to
Copy link

Choose a reason for hiding this comment

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

looks like this was done since you have PhantomData<fn() -> T>

@ehiggs
Copy link

ehiggs commented Nov 25, 2024

@caspermeijn has asked for this to be split into smaller PRs:
#1081 (review)

Reviewing this, I would also expect to see example messages which get extended and validating that before and after changes to the proto file, it works.

e.g.

message TestMessage {
    string name = 1;
}

Then a json payload that satisfies that is then stuck into:

message TestMessage {
    string name = 1;
    int32 age = 2;
}

Or is there an existing test framework and this is using it as another pass with serde enabled and I just don't see how it fits together.

I'm not a committer so take my suggestion with a grain of salt. You've already done an impressive bit of work here!

@cynecx
Copy link
Author

cynecx commented Nov 25, 2024

@caspermeijn has asked for this to be split into smaller PRs:

Yeah, I've mentioned this in my earlier update comment as well. It's definitely on my to-do list! :)

Or is there an existing test framework and this is using it as another pass with serde enabled and I just don't see how it fits together.

I haven't written any specific tests for this yet. That said, I'm not overly concerned because the conformance test suite already exercises this functionality by feeding data (with unknown/missing fields) for decoding/deserialization during our roundtrip conversions. However, having some dedicated tests in our own codebase wouldn't hurt either.

For now, I'll likely prioritize other tasks in the near future. However, I'd love to collaborate on this, if you have any ideas for tests, feel free to share :D

@cynecx
Copy link
Author

cynecx commented Dec 8, 2024

The most recent commits should address almost all conformance failures. There is just one remaining, but that is a rather uninteresting one (Recommended.Proto2.JsonInput.FieldNameExtension.Validator). I've also added some more cleanups and lint fixes.

I think I am approaching a state where I can actually focus on the other TODOs mentioned in my other comments above.

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.

4 participants