Skip to content

feat(generator): add debug_redact to key fields and enable redact in DebugString - #16398

Merged
colinmoy merged 10 commits into
googleapis:mainfrom
colinmoy:feat-generator-debug-redact-keys
Sep 1, 2026
Merged

colinmoy merged 10 commits into
googleapis:mainfrom
colinmoy:feat-generator-debug-redact-keys

Conversation

@colinmoy

@colinmoy colinmoy commented Aug 27, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes: b/510919426

@colinmoy
colinmoy requested a review from a team as a code owner August 27, 2026 19:48

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request updates the discovery-to-proto generator to support mapping 'any' types to google.protobuf.Value or google.protobuf.Struct instead of always defaulting to google.protobuf.Any. It also introduces automatic redaction (debug_redact = true) for string/bytes fields containing 'key' in their name and updates DebugString to respect this option. The review feedback highlights several potential nlohmann::json::type_error exceptions that could occur if JSON Schema elements (like additionalProperties, items, or format) are not objects or strings as expected, recommending explicit type checks. Additionally, it suggests refining the redaction logic with a word-boundary check to prevent false positives on fields containing 'key' as a substring (e.g., 'monkey'), utilizing std::string_view per the repository style guide.

I am having trouble creating individual review comments. Click here to see my feedback.

generator/internal/discovery_to_proto.cc (384-395)

high

In JSON Schema, additionalProperties can be a boolean (e.g., false or true). If items["additionalProperties"] is a boolean, calling .value("type", "") or .contains("format") on it will throw a nlohmann::json::type_error exception (specifically type_error.306 because the JSON value is not an object). We should verify that items["additionalProperties"] is an object before calling these methods.

    if (IsDiscoveryArrayType(*current)) {
      auto const& items = (*current)["items"];
      if (items.is_object() &&
          items.contains("type") && items["type"] == "object" &&
          items.contains("additionalProperties") &&
          items["additionalProperties"].is_object() &&
          items["additionalProperties"].value("type", "") == "any" &&
          !items.contains("format") &&
          !items["additionalProperties"].contains("format")) {
        types_to_import.insert("google.protobuf.Struct");
      } else {
        worklist.push_back(&items);
      }
    }

generator/internal/discovery_to_proto.cc (397-407)

high

Similar to the array items check, additional_properties can be a boolean value in JSON Schema. If it is not an object, calling .contains() on it will throw a nlohmann::json::type_error exception. We should ensure additional_properties is an object before querying its properties.

    if (IsDiscoveryMapType(*current)) {
      auto const& additional_properties = (*current)["additionalProperties"];
      if (additional_properties.is_object() &&
          additional_properties.contains("type") &&
          additional_properties["type"] == "any" &&
          !additional_properties.contains("format") &&
          !current->contains("format")) {
        types_to_import.insert("google.protobuf.Struct");
      } else {
        worklist.push_back(&additional_properties);
      }
    }

generator/internal/discovery_to_proto.cc (363-372)

medium

When checking the "format" field, we should ensure it is actually a string before assigning it to std::string const format. If "format" is present but is not a string (e.g., a boolean or an integer), the implicit conversion to std::string will throw a nlohmann::json::type_error exception.

    if (current->contains("format") && (*current)["format"].is_string()) {
      std::string const format = (*current)["format"];
      if (absl::StartsWith(format, "google.protobuf.")) {
        types_to_import.insert(format);
        // This node is resolved as a protobuf message (e.g. google.protobuf.Any
        // on Status.details items), so do not traverse into its internal
        // additionalProperties or properties.
        continue;
      
    }

generator/internal/discovery_type_vertex.cc (48-56)

medium

If field_json["items"] is not an object, calling .value("type", "") on it will throw a nlohmann::json::type_error exception. We should ensure field_json["items"] is an object before attempting to retrieve its "type".

bool IsStringOrBytes(nlohmann::json const& field_json) {
  std::string const type = field_json.value("type", "");
  if (type == "string" || type == "bytes") return true;
  if (type == "array" && field_json.contains("items") &&
      field_json["items"].is_object()) {
    std::string const item_type = field_json["items"].value("type", "");
    if (item_type == "string" || item_type == "bytes") return true;
  }
  return false;
}

generator/internal/discovery_type_vertex.cc (124-132)

medium

Ensure that v["format"] is actually a string before assigning it to type to prevent a potential nlohmann::json::type_error exception.

  if (type == "any") {
    if (v.contains("format") && v["format"].is_string()) {
      type = v["format"];
    } else {
      type = "google.protobuf.Value";
    }
    return TypeInfo{type, compare_package_name, properties_for_synthesis, false,
                    false};
  }

generator/internal/discovery_type_vertex.cc (165-174)

medium

Ensure that additional_properties and v are objects and their "format" fields are strings before assigning them to map_type to prevent a potential nlohmann::json::type_error exception.

      } else if (map_type == "any") {
        if (additional_properties.is_object() &&
            additional_properties.contains("format") &&
            additional_properties["format"].is_string()) {
          map_type = additional_properties["format"];
        } else if (v.contains("format") && v["format"].is_string()) {
          map_type = v["format"];
        } else {
          map_type = "google.protobuf.Struct";
        }
        return TypeInfo{map_type, compare_package_name,
                        properties_for_synthesis, true, is_message};

generator/internal/discovery_type_vertex.cc (208-228)

medium

Ensure that items and items["additionalProperties"] are objects and their "format" fields are strings before querying them or assigning them to type to prevent potential nlohmann::json::type_error exceptions.

      } else if (type == "any") {
        if (items.is_object() && items.contains("format") &&
            items["format"].is_string()) {
          type = items["format"];
        } else {
          type = "google.protobuf.Value";
        }
        return TypeInfo{type, compare_package_name, nullptr, false, false};
      } else if (type == "object" && items.is_object() &&
                 items.contains("properties")) {
        // Synthesize a nested type for this array.
        type = CapitalizeFirstLetter(field_name + "Item");
        return TypeInfo{type, compare_package_name, &items, false, true};
      } else if (type == "object" && items.is_object() &&
                 items.contains("additionalProperties") &&
                 items["additionalProperties"].is_object() &&
                 items["additionalProperties"].value("type", "") == "any") {
        if (items.contains("format") && items["format"].is_string()) {
          type = items["format"];
        } else if (items["additionalProperties"].contains("format") &&
                   items["additionalProperties"]["format"].is_string()) {
          type = items["additionalProperties"]["format"];
        } else {
          type = "google.protobuf.Struct";
        }
        return TypeInfo{type, compare_package_name, nullptr, false, false};

generator/internal/discovery_type_vertex.cc (510-514)

medium

The current substring check absl::StrContains(field_name, "key") is highly prone to false positives and will over-redact fields that happen to contain "key" as a substring but are not actual keys (e.g., monkey, keyboard, hockey, keypad, keynote).

Since field_name is already formatted as snake_case by the generator, we can perform a precise word-boundary check to ensure "key" is matched only as a standalone word (e.g., kms_key_name, raw_key, rsa_encrypted_key, or key itself). This also eliminates the need to check json_field_name separately.

You can add a helper function ContainsKeyWord in the anonymous namespace:

bool ContainsKeyWord(std::string_view s) {
  for (std::size_t pos = s.find("key"); pos != std::string_view::npos;
       pos = s.find("key", pos + 1)) {
    bool const prefix_ok = (pos == 0 || s[pos - 1] == '_');
    bool const suffix_ok = (pos + 3 == s.size() || s[pos + 3] == '_');
    if (prefix_ok && suffix_ok) return true;
  }
  return false;
}
  if (IsStringOrBytes(field_json) && ContainsKeyWord(field_name)) {
    field_options.emplace_back("debug_redact", "true");
  }
References
  1. Prefer using std::string_view instead of absl::string_view in C++ code. (link)

}

if (current->contains("format")) {
if (current->contains("format") && (*current)["format"].is_string()) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If the discovery document contains a format field that is not a string, this is a fundamental problem that the generator cannot recover from. We should emit an error and exit.

auto const& items = (*current)["items"];
if (items.contains("type") && items["type"] == "object" &&
items.contains("additionalProperties") &&
if (items.is_object() && items.contains("type") &&

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Similarly, if this isn't an object, then the discovery document is invalid. Emit an error and exit.

Same below. If checks to verify the structure of the document fail, these are non-recoverable.

Comment thread generator/internal/discovery_type_vertex.cc
@codecov

codecov Bot commented Aug 28, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.41520% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 92.27%. Comparing base (f69f92f) to head (bb5ddc5).

Files with missing lines Patch % Lines
...oogle/cloud/internal/debug_string_protobuf_test.cc 96.15% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #16398      +/-   ##
==========================================
+ Coverage   92.24%   92.27%   +0.02%     
==========================================
  Files        2246     2246              
  Lines      212136   212297     +161     
==========================================
+ Hits       195692   195888     +196     
+ Misses      16444    16409      -35     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

}

bool ContainsKeyWord(std::string_view s) {
auto const tokens = absl::StrSplit(s, '_');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Instead of auto, explicitly name the type, especially with absl::StrSplit as it can have several different return types. I would imagine we want a std::vector<std::string_view> here.

bool ContainsKeyWord(std::string_view s) {
auto const tokens = absl::StrSplit(s, '_');
return std::any_of(tokens.begin(), tokens.end(),
[](absl::string_view token) { return token == "key"; });

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Prefer std::string_view to absl::string_view. Any absl type that has a std type equivalent should usually be preferred.

absl::StrCat("\"", field_name, "\""));
}

if (IsStringOrBytes(field_json) && ContainsKeyWord(field_name)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looking through the emitted protos, I think I underestimated how many fields named key would be false positives for redacting. It looks like there's a lot of types that contain both key and value fields that are simply a map of items. Let's see if we can improve our logic to determine when to redact, by NOT redacting fields named exactly key when the field value is present as a sibling field in the message type.

@colinmoy
colinmoy requested a review from scotthart August 31, 2026 22:20
}

Status
DiscoveryTypeVertex::FormatPropertiesHelper( // NOLINT(misc-no-recursion)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let's create an internal cleanup issue to look at refactoring the parameters to this functions. 13 separate parameters is too many. We'll want to consider grouping some into structs.

@colinmoy
colinmoy merged commit 3aca29a into googleapis:main Sep 1, 2026
63 checks passed
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