Skip to content

[DT-4274][risk=low] Add consent-code summaries and a null-serializing Gson (1/3) - #3189

Merged
otchet-broad merged 5 commits into
developfrom
otchet-dt-4274-utils
Oct 28, 2026
Merged

otchet-broad merged 5 commits into
developfrom
otchet-dt-4274-utils

Conversation

@otchet-broad

@otchet-broad otchet-broad commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Addresses

https://broadworkbench.atlassian.net/browse/DT-4274

Security risk: low — adds two helper methods. Nothing calls them yet.

Summary

Part 1 of 3 of the DT-4274 showcase catalog projection, split from #3184 at reviewer request. Merge order: this PR, then #3190, then #3184.

  • TranslationUtil.summaryCodes(DataUse) returns the consent codes of a DataUse (GRU, HMB, DS, OTHER, ...) in translateSummary order, without free-text values or disease labels. Two differences from translateSummary are deliberate and documented in the Javadoc: a code appears once (a primary and a secondary OTHER merge), and DS appears whenever there are disease restrictions, even when no ontology label resolves.
  • GsonUtil.getNullSerializingInstance() writes null fields as null instead of dropping them, so a response always has the full key set. [DT-4323][risk=no] Write registration enums as their documented values #3174 removed the earlier buildGsonNullSerializer().

The review discussion of summaryCodes is on #3184 (duplication with translateSummary, differences from the DUOS summary).

Testing

TranslationUtilTest (including testSummaryCodesMatchTranslateSummary, which compares both methods for a fully populated DataUse) and GsonUtilTest pass locally. The code is unchanged from #3184.


Have you read CONTRIBUTING.md lately? If not, do that first.

  • Label PR with a Jira ticket number and include a link to the ticket
  • Label PR with a security risk modifier [no, low, medium, high]
  • PR describes scope of changes
  • Get a minimum of one thumbs worth of review, preferably two if enough team members are available
  • Get PO sign-off for all non-trivial UI or workflow changes
  • Verify all tests go green
  • Test this change deployed correctly and works on dev environment after deployment

🤖 Generated with Claude Code

… Gson

TranslationUtil.summaryCodes returns the consent codes of a DataUse
without free-text values. GsonUtil.getNullSerializingInstance writes
null fields as null. Split from #3184 at reviewer request; the showcase
catalog projection uses both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@otchet-broad
otchet-broad requested a review from a team as a code owner October 27, 2026 17:38
@otchet-broad
otchet-broad requested review from fboulnois and rushtong and a balanced review from Copilot and removed request for a team October 27, 2026 17:38

Copilot AI left a comment

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.

🟢 Approval recommended

The isolated, additive helpers match their documented behavior, include targeted tests, and have no identified blocking issues.

No open issues 🎉

What changed in this PR

Adds two utility helpers for the upcoming showcase catalog projection without changing existing production call paths.

Changes:

  • Adds ordered, distinct consent-code summaries without free text or disease labels.
  • Adds a Gson instance that preserves null fields while leaving existing serializers unchanged.
  • Adds tests for summary-code parity, null input, and null-field serialization.
File Description
src/​test/​java/​org/​broadinstitute/​consent/​http/​util/​gson/​GsonUtilTest.java Tests null-field preservation and existing serializer behavior.
src/​test/​java/​org/​broadinstitute/​consent/​http/​matching/​TranslationUtilTest.java Tests summary-code parity and null input.
src/​main/​java/​org/​broadinstitute/​consent/​http/​util/​gson/​GsonUtil.java Adds a reusable null-serializing Gson instance.
src/​main/​java/​org/​broadinstitute/​consent/​http/​matching/​TranslationUtil.java Adds code-only summaries with documented ordering and deduplication.

Review effort: Balanced

@rushtong rushtong left a comment

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.

Review of the helpers. The code does what the Javadoc says. My main concern is drift between summaryCodes and translateSummary (two copies of the same 19 rules) and the test gaps for the two documented differences. The other comments are minor.

🤖 Generated with Claude Code

*/
// Suppress warning for method complexity (S3776). Mirrors translateSummary.
@SuppressWarnings({"java:S3776"})
public static List<String> summaryCodes(DataUse dataUse) {

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.

Drift risk. This method copies the 19 conditions of translateSummary by hand. If someone adds a code (for example aiLlmUse) or changes a condition in one method only, the dataset page and the catalog disagree, and no compile error or test failure shows it.

Suggestion: put the rules in one table (predicate, code, description) and let both methods read it. Then summaryCodes cannot drift from translateSummary.

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.

Fixed in 67ce583. translateSummary and summaryCodes now read one SUMMARY_RULES table. Each row holds the code, primary or secondary, the condition and the description. A code added to the table appears in both methods.

// Suppress warning for method complexity (S3776). Mirrors translateSummary.
@SuppressWarnings({"java:S3776"})
public static List<String> summaryCodes(DataUse dataUse) {
Set<String> codes = new LinkedHashSet<>();

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.

Nit: create the set after the null check. For a null DataUse, the method creates a set that it never uses.

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.

Fixed in 67ce583. summaryCodes returns for a null DataUse before it builds anything. It is now a stream over SUMMARY_RULES.

return List.of();
}
if (BooleanUtils.isTrue(dataUse.getGeneralUse())) {
codes.add("GRU");

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.

Nit: the code strings ("GRU", "DS", "OTHER", "POP-M", ...) are literals here and in translateSummary. Catalog clients can filter on these values, so they are an API contract. Shared constants (or an enum) prevent a typo or a rename in one place only.

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.

In 67ce583, each code string is now written once, in SUMMARY_RULES, which both methods read. OTHER is used by two rows, so it is a constant (this also clears Sonar S1192). I did not add a public enum yet, because no client reads these codes. S2.2 can promote them if the API needs one.

codes.add("GRU");
}
if (dataUse.getDiseaseRestrictions() != null && !dataUse.getDiseaseRestrictions().isEmpty()) {
codes.add("DS");

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 Javadoc says this difference is deliberate, but please confirm the product intent. For a disease ID that does not resolve (bad or retired ID, or an ontology lookup that returns nothing), the catalog shows DS but the dataset summary shows no DS row. Users then see two different data-use statements for one dataset.

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.

Yes, this is intended. DS means the dataset has disease restrictions, and that is true whether or not the ontology service returns a label. Hiding DS would make a restricted dataset look less restricted in a public catalog. The dataset page leaves the row out only because it has no label to print. testSummaryCodesKeepDSWithoutLabel now pins this. I'll confirm with the PO during the S2.1 field review.

if (BooleanUtils.isTrue(dataUse.getPopulation())) {
codes.add("POP");
}
return List.copyOf(codes);

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.

DataUse has fields that neither this method nor translateSummary maps to a code: aiLlmUse, illegalBehavior, sexualDiseases, stigmatizeDiseases, vulnerablePopulations, psychologicalTraits, notHealth. The gap already exists, but this PR puts it on a new public surface. For example, a dataset that prohibits AI/LLM use shows no matching code in the catalog. Is that intended for the showcase?

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.

Not decided for the showcase yet. These seven fields have no code in translateSummary either, so today the catalog and the dataset page agree. The new test lists them in FIELDS_WITHOUT_CODE and fails when a new DataUse field has no code and is not listed. I'll add this question to the steward and AppSec review of the S2.1 fields. If they want a code (for example for aiLlmUse), it is one row in SUMMARY_RULES.

void testSummaryCodesMatchTranslateSummary(String gender) {
OntologyTerm term = initializeDiseaseTerm();
DataUse dataUse =
new DataUseBuilder()

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 parity test sets a fixed list of builder fields. If someone adds a branch for a new field to translateSummary, this builder does not set that field. Both methods then omit the code, and the test still passes while real data disagrees. One option: a test that sets every DataUse field (by reflection), so that a new field makes the test fail.

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.

Added in 67ce583: testEveryDataUseFieldHasACodeOrIsListed. It sets each DataUse field on its own by reflection and checks that both methods return the same codes. It fails if a field gives no code and is not in FIELDS_WITHOUT_CODE. I checked that it fails when notHealth is removed from that list.

}

@Test
void testSummaryCodesNullDataUse() {

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.

Please add tests for the two documented differences:

  1. DS when no ontology label resolves (stub findByTermIds to return an empty list) — expect DS in summaryCodes and no DS in translateSummary.
  2. Only secondaryOther set, and both other and secondaryOther set — expect one OTHER in the correct position.

Now the only test uses a label that resolves, and it checks the merge only through distinct().

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.

Added in 67ce583: testSummaryCodesKeepDSWithoutLabel (blank label: DS in summaryCodes, nothing in translateSummary), testSummaryCodesSecondaryOtherOnly, and testSummaryCodesMergePrimaryAndSecondaryOther ([HMB, OTHER, NMDS]).

.registerTypeAdapterFactory(new JsonValueEnumTypeAdapterFactory())
.create();

private static final Gson NULL_SERIALIZING_INSTANCE =

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.

Minor: this is the third Gson variant. It differs from REGISTRATION_INSTANCE only by the @JsonValue enum adapter. Please say in the Javadoc when to use each one. If a caller serializes an enum with this instance, it gets the Java name, not the documented value.

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.

In 67ce583, added Javadoc to getInstance and getNullSerializingInstance. It says which one to use when, and that only getRegistrationInstance writes enums by their @JsonValue.

@rushtong
rushtong self-requested a review October 27, 2026 18:02

@fboulnois fboulnois left a comment

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.

👍

…d summaryCodes

Address review on #3189: both methods now read SUMMARY_RULES, so a code
cannot be added to one and not the other. A reflection test fails when a
DataUse field has no code and is not listed as unmapped. New tests pin
DS without an ontology label and the OTHER merge. GsonUtil documents
when to use each instance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
otchet-broad added a commit that referenced this pull request Oct 27, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
otchet-broad added a commit that referenced this pull request Oct 27, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@rushtong rushtong left a comment

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.

Thanks for the updates, looks great 👍🏽

otchet-broad and others added 3 commits October 27, 2026 18:03
…ord (2/3) (#3190)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…gibility filter (3/3) (#3184)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sonar java:S1192: "string" appeared six times in ShowcasePublicField.
The JSON type names move to a private JsonType holder class, because
enum constants cannot refer to the enum's own static fields. Every
getType() value is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@otchet-broad
otchet-broad merged commit 2ed9be0 into develop Oct 28, 2026
11 checks passed
@otchet-broad
otchet-broad deleted the otchet-dt-4274-utils branch October 28, 2026 00:32
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