Skip to content

Process a top-level JSON list once per record in JsonDataProcessor - #2205

Open
arpitjain099 wants to merge 2 commits into
data-privacy-stack:mainfrom
arpitjain099:fix/json-processor-list
Open

Process a top-level JSON list once per record in JsonDataProcessor#2205
arpitjain099 wants to merge 2 commits into
data-privacy-stack:mainfrom
arpitjain099:fix/json-processor-list

Conversation

@arpitjain099

Copy link
Copy Markdown

Change Description

JsonDataProcessor._process puts the isinstance(data, list) branch inside the per-key loop:

for key, operator_callable in key_to_operator_mapping.items():
    keys = key.split(".")
    if isinstance(data, list):
        for item in data:
            self._process(item, key_to_operator_mapping)   # full mapping, every key
    else:
        ...

For a top-level list of records the outer loop runs once per mapped key, and each pass re-processes the entire list with the full mapping. So every operator is applied N times per value, where N is the number of keys in the entity mapping and should be 1. The dict path is correct.

The class docstring advertises arbitrary nesting of dictionaries and lists and the list branch is already there, so a top-level array of records is an intended input shape. Idempotent operators (replace, single-shot mask) hide the problem, which is why the existing tests pass. Non-idempotent ones do not: a custom lambda runs N times, hash becomes a hash of a hash, encrypt double-encrypts, and surrogate maps built for de-anonymization end up wrong.

The fix moves the list branch above the loop and returns, so each record is visited once with the whole mapping.

Issue reference

None, found while reading the structured processors.

Checklist

  • I have reviewed the contribution guidelines
  • I agree to follow this project's Code of Conduct
  • I confirm that I have the right to submit this contribution and that it does not knowingly contain proprietary or confidential code.
  • My code includes unit tests
  • All unit tests and lint checks pass locally
  • My PR contains documentation updates / additions if required

Added TestJsonDataProcessor::test_process_top_level_list_applies_each_operator_once, which uses a non-idempotent custom operator over a two-key mapping. Before the change it produces Alice!! instead of Alice!; after the change the whole presidio-structured suite is 30 passed.

JsonDataProcessor._process handled the list case inside the per-key loop,
so a top-level list was re-processed in full for every key in the entity
mapping. Each operator ran N times per value for an N-key mapping. Move
the list branch above the loop and return.

Signed-off-by: Arpit Jain <arpitjain099@gmail.com>
@SharonHart
SharonHart requested a review from Copilot July 29, 2026 09:10

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.

Pull request overview

This PR fixes JsonDataProcessor._process so a top-level JSON list (list of records) is processed once per record, instead of re-processing the entire list once per mapped key (which would apply operators multiple times per value and break non-idempotent operators).

Changes:

  • Move the isinstance(data, list) handling above the per-key loop in JsonDataProcessor._process and return early after processing each item once.
  • Add a regression test using a non-idempotent custom operator to ensure each operator is applied exactly once for top-level list inputs.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
presidio-structured/presidio_structured/data/data_processors.py Processes top-level lists once per item to prevent repeated operator application per mapped key.
presidio-structured/tests/data/test_data_transformers.py Adds a regression test validating single application of operators for top-level list inputs.

Comment on lines +213 to 217
if text_to_operate_on:
if isinstance(text_to_operate_on, list):
for text in text_to_operate_on:
operated_text = self._operate_on_text(text, operator_callable)
self._set_nested_value(data, keys, operated_text)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants