Skip to content

Re-attach proto2 extensions when loading meta-data from JSON - #4477

Open
hatyo wants to merge 2 commits into
FoundationDB:mainfrom
hatyo:json-metadata-extensions
Open

Re-attach proto2 extensions when loading meta-data from JSON#4477
hatyo wants to merge 2 commits into
FoundationDB:mainfrom
hatyo:json-metadata-extensions

Conversation

@hatyo

@hatyo hatyo commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

The load schema template ... from <file>.json command parses meta-data with JsonFormat, which implements the proto3 JSON mapping and, as its javadoc says, treats proto2 extensions as if they did not exist. The extensions are present in the file, they are just dropped on the way in, and with them goes information the meta-data cannot do without. A vector field is a bytes field whose precision and dimensions live in an extension of google.protobuf.FieldOptions, so a schema template loaded this way silently ends up with a plain bytes field where a vector was meant.

This change adds a pass over the meta-data that runs after the ordinary parse and walks the JSON alongside the builder it was parsed into, setting every key that names a known extension of the message at hand and recursing into the message-valued fields, so that options nested anywhere in the descriptor are covered. The value of an extension is itself an ordinary message, so JsonFormat handles its contents; only the hook-up was missing. The extensions are looked up by full name in a registry built from the generated classes of the resolved dependencies together with the option protos of the record layer, which keeps the meta-data format diffable JSON rather than raw bytes.

This fixes #4478.

`load schema template ... from <file>.json` parses the meta-data with
`JsonFormat`, which implements the proto3 JSON mapping and therefore
discards proto2 extensions. Options carried by an extension are lost
even though the file spells them out, which silently turns a vector
field into a plain bytes field, and drops the CloudKit extensions of
`Index`, `RecordType` and `FormerIndex` along the way.

After the ordinary parse, walk the JSON alongside the builder it was
parsed into and set every key that names a known extension of the
message at hand. The extensions are looked up in a registry built from
the generated classes of the dependencies the loader already resolves.
@hatyo hatyo added the testing improvement Change that improves our testing label Aug 19, 2026
@hatyo
hatyo marked this pull request as ready for review August 19, 2026 17:44
@hatyo
hatyo requested a review from ScottDugas August 19, 2026 17:45
// the fields of the extension value are regular fields, so JsonFormat handles them just fine
final Message.Builder valueBuilder = Objects.requireNonNull(defaultInstance).newBuilderForType();
try {
JsonFormat.parser().ignoringUnknownFields().merge(value.toString(), valueBuilder);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why ignore unknown fields?

private static Object extensionValue(@Nonnull Descriptors.FieldDescriptor extension,
@Nullable Message defaultInstance,
@Nonnull JsonElement value) {
switch (extension.getJavaType()) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should this be a switch expression?

return byName;
}
for (Descriptors.FieldDescriptor field : descriptor.getFields()) {
if (field.getJsonName().equals(name)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is it possible that the json name of a field equals the regular name of a different field?

return result;
}

private static RecordMetaData loadRecordMetaDataFromJson(String jsonFileName) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It seems worthwhile to add at least two tests:

  1. This method specifically parsing out a sample metadata with some vectors
  2. A YamlTest that depends on a json metadata that has vectors in it

It might also make sense to pull this out into a separate utility class

@Nonnull ExtensionRegistry registry) {
final Descriptors.Descriptor descriptor = builder.getDescriptorForType();
for (Map.Entry<String, JsonElement> entry : json.entrySet()) {
final ExtensionRegistry.ExtensionInfo extension = registry.findImmutableExtensionByName(entry.getKey());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This gets called for every json key, right, so it would get called for things like name/number/label/type
?

} catch (InvalidProtocolBufferException e) {
throw new RuntimeException("unable to parse value of extension " + extension.getFullName(), e);
}
return valueBuilder.build();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It's irrelevant for the vector case, and probably any other existing case, but should this also call mergeExtension, recursing in case the extension has fields with extensions.
I'm ok with concluding that it would perhaps be nice to do later when we have a use case

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

Labels

testing improvement Change that improves our testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Loading meta-data from JSON silently drops proto2 extensions

2 participants