Skip to content

feat: use proto presence information to inform nullability#43

Closed
devoncarew wants to merge 1 commit intogoogleapis:mainfrom
devoncarew:nullability
Closed

feat: use proto presence information to inform nullability#43
devoncarew wants to merge 1 commit intogoogleapis:mainfrom
devoncarew:nullability

Conversation

@devoncarew
Copy link
Copy Markdown
Contributor

Use proto presence information to inform nullability:

This is a draft PR as I'm still working on the generator portion of the changes. I think this is still interesting to look at, esp. at how it effects the code using the APIs (the examples, tests, ...).

I'll include more info in a non-draft PR, or in the generator PR, but:

  • proto3 defaults to implicit presence for primitive types; so, it does not track presence, and, values can always be returned from a field whether they'd been set or not (ints default to 0, strings to empty strings, ...). We generate this as non-null fields; when decoding from json, we set the default type value if no value is present
  • lists and maps have implicit presence; we generate these as non-null fields, and use empty lists and maps if they are not present in the decoded json
  • if a proto field is marked as optional, that promotes it to explicit presence; you can determine whether a value had been set or not. We generate those as nullable Dart fields.

We're making the decision in this generator to not serialize to json fields that have implicit presence and where they have the default value for that type. I believe this is correct, but it would be good to get independent confirmation / some trial and error here to get a bit more confidence.

cc @brianquinlan and @natebosch

print('<No textual response>');
} else {
print(parts.map((p) => p.text ?? '').join(''));
print('<= ' + parts.map((p) => p.text).join(''));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The nice thing is that you won't need the map here because p.text can never be null. But text is part of a oneof. How can you tell if that field is set or not? The Python API makes it nullable: https://github.com/googleapis/python-genai/blob/7e0beba3afb4329660d7377c5276194bf68cbcef/google/genai/types.py#L1293

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't think we really support oneofs for the Dart backend yet? Or at least not explicitly - perhaps we mostly do as long as everything is nullable.

I can do some checks for oneof and keep those fields nullable. We may also want to write out whether a field is part of a oneof (w/ at least one other field)? So for text:

  /// Inline text.
  ///
  /// Only one of [text] and [inlineData] may be set.
  final String? text;

print('=> $prompt');
final result = await service.generateContent(request);
final parts = result.candidates?[0].content?.parts;
final parts = result.candidates[0].content?.parts;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[optional]

Suggested change
final parts = result.candidates[0].content?.parts;
final parts = result.candidates.first.content?.parts;


final result = await service.generateContent(request);
final parts = result.candidates?[0].content?.parts;
final parts = result.candidates[0].content?.parts;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[optional]

Suggested change
final parts = result.candidates[0].content?.parts;
final parts = result.candidates.first.content?.parts;


await for (final result in service.streamGenerateContent(request)) {
final parts = result.candidates?[0].content?.parts;
final parts = result.candidates[0].content?.parts;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[optional]

Suggested change
final parts = result.candidates[0].content?.parts;
final parts = result.candidates.first.content?.parts;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

👍 will do

);
}

@override
Object toJson() {
return {'cachedContent': cachedContent.toJson()};
return {
if (cachedContent != null) 'cachedContent': cachedContent!.toJson(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We can avoid the !

Suggested change
if (cachedContent != null) 'cachedContent': cachedContent!.toJson(),
if (cachedContent case final content?) 'cachedContent': content.toJson(),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Let's track that in #41; the PR for the generator changes already has lots of things going on, and I haven't fully groked pattens myself.

updateMask: decodeCustom(json['updateMask'], FieldMask.fromJson),
);
}

@override
Object toJson() {
return {
'cachedContent': cachedContent.toJson(),
if (cachedContent != null) 'cachedContent': cachedContent!.toJson(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
if (cachedContent != null) 'cachedContent': cachedContent!.toJson(),
if (cachedContent case final content?) 'cachedContent': content.toJson(),

}

extension BoolExtension on bool {
bool get isNotDefault => this != false;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This looks like a good choice to me. There is a downside in having this API that leaks into auto-complete all over, but I think I still prefer it over the result.hasFoo ? result.roo : null; type API.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks! The good news here is that the generated code uses these extensions in their implementations, but the extensions aren't currently in anything that the user would import. So the symbols won't be visible to the users by default.

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