Compatibility

Does Obfuscation Break CSV and Spreadsheet Imports?

An import feature is a contract with a stranger. Somebody exports a file from an accounting package, a partner system or a spreadsheet, and the first row of that file names the columns. Your code turns those names into object properties and reads them, which looks like the most ordinary code in the importer and is the one part of it that a rename pattern can reach.

What was measured

One sample file, driven through five protection profiles for the base column and through both member-renaming profiles for the rest. It parses a small exported file with a header row of sku,qty,unit_price,currency, reads columns off the resulting rows, totals a line-item calculation, renders a report header from a column-label map, runs a required-column check, and finally writes a normalised record back out as JSON.

The header row is not authored by the sample. It stands in for a file some other system exported, and the row objects are built from that text at runtime -- a loop that assigns each cell to whatever property name the header column carries -- which is not a rename site. What the sample does author is the column-label map, the normalised output record and the container the batch is held in, and those are rename sites. Having both sides in one sample is the point: it shows exactly which half moves.

The base column is clean. On all five profiles the sample reproduced the unprotected output character for character: the parsed key list, the column reads, the total, the report header, the required-column verdict and the serialised record. Protection on its own does not change how a CSV import behaves.

The column disappears from the output rather than arriving wrong

With member renaming on and a pattern matching the product-code column, the direct read went from first-sku=A-1 to first-sku=undefined, and the same read through the batch container went the same way. That much is the ordinary shape of this defect.

The output record is where something new happens. It went from {"sku":"A-1","quantity":2,"amount":19.98} to {"quantity":2,"amount":19.98}. The product code is not wrong in the emitted record; it is not there. JSON.stringify omits any property whose value is undefined, so a renamed read does not produce a null or an empty string that a downstream schema check would catch -- it removes the field from the document entirely.

That matters because of what happens next. A receiving system validating against a schema will report a required property as missing, pointing at a field your code demonstrably sets, on a line of source that plainly sets it. A receiving system without a schema will insert a row with a null product code. Neither outcome looks like an obfuscation problem from either end, which is why it is worth knowing the shape in advance.

Totals go to NaN, and NaN serialises as null

The arm matching the quantity and unit-price columns is the arithmetic case. The line-item total went from total=45.98 to total=NaN, because both operands read as undefined and any arithmetic on undefined yields NaN. A total that prints as NaN in a summary is at least visibly wrong to anyone looking at it.

The record written back out is quieter. It went to {"sku":"A-1","quantity":null,"amount":null}, because JSON.stringify converts NaN to null. So the same run produced two different disguises for the same failure: the field vanishes when the value is undefined, and becomes null when the value is NaN. Nothing in either document says the number was ever there.

One consequence deserves stating plainly for anyone whose import writes to a ledger or an inventory. A quantity of null is not a small error, and it will be accepted by most database columns and most spreadsheet cells without complaint. The failure that reaches a human is a report that does not add up, several steps and possibly several days downstream of the build that caused it.

The report still looks right, and the validator still says the file is fine

Two arms produced no change at all, and both are more interesting than the arms that did. The required-column check -- the guard whose entire purpose is to catch a file with the wrong columns -- still reported absent-columns=none in every arm. It compares a list of column names held as strings against the keys the parsed row actually carries, and both of those came from the file. Neither is a rename site, so the check is correct, it is just answering a question about the file rather than about your ability to read it.

The report header rendered correctly too, at Item|Quantity|Unit price, in the arm where the very columns it labels were unreadable. The labels are looked up by key over the map's own live keys, so the lookup stayed self-consistent while the data underneath it was gone. A report with correct headers and empty or nonsensical rows is a specific kind of bug report to receive.

The container arm is the boundary case. A pattern matching the object that holds the parsed rows produced no difference, because that container is written and read in the same file and both ends were renamed together. This is the reliable predictor: a name authored and consumed inside your own bundle is safe under renaming, and a name that one party writes and another reads is not.

What the ticket says when this happens

It is worth knowing how this arrives, because it does not arrive as an obfuscation question. The most common report is that the import worked and the rows are empty: no error dialog, no rejected file, a success message and a table of blanks. That is the undefined read reaching the screen, and the reason nobody suspects the build is that the file is visibly fine when opened in a spreadsheet.

The second report is a total that does not add up, or a figure displayed as NaN. Support will reasonably ask the customer to re-export the file, and the re-export will behave identically, which burns a day. The third report comes from a system rather than a person: a downstream service rejecting your records for a missing required property, on a field your code sets on a line anyone can point at in a review.

None of those three descriptions contains the word obfuscation, and none of them is likely to reach the engineer who changed the member pattern. The connection is only obvious if somebody has read the parsed object and the emitted record from the protected build, which is why the check below is worth running once at the point of the change rather than being discovered from the ticket queue.

Why column names are the worst case for a broad pattern

There is a reason this area is worth its own measurement rather than a line in a general article. Column names are the shortest, most generic identifiers in any codebase: sku, qty, id, name, date, total, amount, email. A member pattern written with the reasonable-sounding intent of covering the application's data model matches every one of them, and it matches them in code the author of the pattern was not thinking about.

The import path is also, in most products, the least exercised code that touches customer data. It runs during onboarding and at month end rather than on every page load. It usually sits behind a file upload, which is exactly the interaction that end-to-end suites skip. And the file that would reproduce the problem belongs to a customer, so it is not in your repository and cannot be added to it.

All of which makes a fixture the cheapest possible insurance. One small file with the same header row as a real export, checked in beside the importer, run against the protected artifact rather than the source, and asserted on the parsed values and the emitted record. That single test distinguishes the three symptoms measured here -- an undefined read, a key missing from the output document and a total of NaN -- from the ordinary data problems that an import feature produces on its own.

It is worth adding that none of this is exotic configuration. The arms that failed used the same member renaming that this site recommends for hiding an application's internal structure; nothing unusual was switched on. The difference between a safe and an unsafe pattern is entirely in what it matches, and column names look exactly like the internal names you want it to cover.

What this means in practice

The headline stays narrow. Protection does not break CSV imports; all five profiles reproduced the output exactly. Member renaming reaches the import only when the pattern matches the column names, and column names are exactly the kind of short, common, lower-case identifiers -- sku, qty, id, name, date -- that a broad pattern sweeps up by accident.

There is a second direction worth mentioning, because an importer is usually also an exporter. The arm matching the names in the normalised output record -- names this sample owns at both ends -- changed nothing inside the program and rewrote the document it produces: {"sku":"A-1","_0x1":2,"_0x2":19.98}. Your own code round-trips that happily. The partner reading your export sees two columns they have never heard of and two of theirs missing. Column names you emit are a published interface even when no one has ever called them one.

The check takes one run and needs no test framework. Import a file you know the contents of using the protected build, then look at the parsed record and the record you write out -- not at the source. A column missing from the output document, a numeric column of null, or a total of NaN each mean a column name was renamed. Put the header names in the reserved list, and anchor the member pattern to identifiers distinctive to your application so it cannot match them in the first place.

Frequently asked questions

Does obfuscation break CSV imports?

Not in the default configuration. A sample that parses an exported file, reads its columns, totals a line-item calculation, renders a report header and writes a normalised record produced identical output on all five protection profiles measured. CSV imports become a surface only when member renaming is on and the pattern matches the column names.

Why is my CSV column undefined after obfuscation?

Because the read was renamed and the file still carries the exporter's header. Row objects are built from the header text at runtime, which is not a rename site, so the emitted code asks each row for a name that exists only in your source. The read returns undefined and nothing is thrown.

Why did a field disappear from my exported JSON?

Because JSON.stringify omits properties whose value is undefined. A renamed column read produces undefined, so the field is not written into the document at all. In the measured run the record went from including a product code to not containing that key, which downstream looks like a required property that your code never set.

What happens to totals and numeric columns?

They become NaN in memory and null on the wire. Arithmetic on undefined yields NaN, so the measured total went from 45.98 to NaN, and the serialised record carried null for both numeric fields because JSON.stringify converts NaN to null. Most database columns and spreadsheet cells accept null without complaint.

Will my required-column validator catch this?

No, and it is right not to. That check compares a list of expected column names against the keys the parsed file actually carries. Both come from the file, neither is a rename site, so it correctly reports that the file is fine. It is answering a question about the import file, not about whether your code can read it.

Are the columns I export affected as well?

Yes, in the opposite direction. Names in the record you write out are authored in your source, so renaming rewrites them and your own code keeps working perfectly. The measured export carried two generated column names in place of the real ones. Whoever consumes your export sees columns they do not recognise and columns they expected missing.

How do I verify an import against a protected build?

Run a file whose contents you know through the protected build and inspect the parsed rows and the record you write out, rather than the source. Undefined reads, a missing key in the output document, a null numeric column or a NaN total each indicate a renamed column name. Add the header names to the reserved list or anchor the member pattern away from them.

Related reading