Hardcoding request bodies and test data inside feature files is fine for a handful of scenarios. Once you have dozens of tests and shared payloads, it becomes a maintenance problem: changing a field name means finding and editing every feature file that mentions it. The read() function solves this by loading JSON, CSV, and JavaScript files into variables — data lives in dedicated files, feature files stay clean.
The read() function
read() loads a file and returns its parsed content:
* def userData = read('user-data.json')
* def config = read('classpath:config/test-config.json')
* def users = read('users.csv')Two path forms: a bare path like 'user-data.json' is resolved relative to the feature file's own location on disk. A classpath: prefix resolves from the root of src/test/java (or src/test/resources if you have a separate resources directory). Use classpath: for files shared across multiple feature files in different packages.
JSON files
The most common use case — large or reusable request payloads:
// create-user-request.json
{
"name": "Alice Smith",
"email": "alice@test.com",
"role": "admin",
"address": {
"street": "10 Downing Street",
"city": "London",
"postcode": "SW1A 2AA"
}
}Scenario: Create a user from a file
Given path 'users'
And request read('create-user-request.json')
When method post
Then status 201read('create-user-request.json') returns a JavaScript object — the same as if you'd written the JSON inline. You can immediately navigate it with dot notation or pass it to request directly.
You can also load an expected response shape from a file and use it in a match:
And match response == read('expected-user-response.json')This keeps the full expected response out of the feature file, which is especially useful when asserting on large, complex JSON structures.
Embedded expressions in JSON files
Files support the same #(variable) embedded expressions as inline JSON. When Karate reads the file, it substitutes any #(...) markers using variables in the current scope:
// create-user-request.json
{
"name": "#(name)",
"email": "#(email)",
"role": "#(role)"
}* def name = 'Alice'
* def email = 'alice@test.com'
* def role = 'admin'
And request read('create-user-request.json')Karate reads the file, substitutes the variables, and the request body becomes { "name": "Alice", "email": "alice@test.com", "role": "admin" }. This pattern lets one request file serve many scenarios with different data — you set the variables, then read().
CSV files
read() with a .csv file returns a list of objects, one per row. The header row becomes the field names:
name,email,role
Alice,alice@test.com,admin
Bob,bob@test.com,tester
Carol,carol@test.com,viewer
* def users = read('users.csv')
* match users[0].name == 'Alice'
* match users[1].role == 'tester'
* match karate.sizeOf(users) == 3Every value in a CSV is a string — there is no type inference. If you need numeric IDs from a CSV, cast them: * def id = parseInt(users[0].id). For CSV used with Scenario Outline, Karate handles the string-to-type conversion automatically inside the Examples table.
JavaScript helper files
You can load a .js file and call functions from it. This is useful for generating dynamic test data or encoding values:
// helpers.js
function generateEmail(prefix) {
return prefix + '-' + java.lang.System.currentTimeMillis() + '@test.com';
}
function toBase64(str) {
var Base64 = Java.type('java.util.Base64');
return Base64.getEncoder().encodeToString(str.getBytes('UTF-8'));
}* def helpers = read('classpath:common/helpers.js')
* def email = helpers.generateEmail('test')
* def encoded = helpers.toBase64('alice:password')The file is evaluated as a JavaScript module. Functions exported from it are available as properties of the returned object. Keep helper files in a common/ folder under src/test/java so they're reachable via classpath:.
How file reading fits into the test flow
Organising test data files
A clean layout keeps files findable:
src/test/java/
├── karate-config.js
├── common/
│ └── helpers.js ← shared utility functions
├── users/
│ ├── UsersRunner.java
│ ├── users.feature
│ ├── create-user-request.json ← request body
│ └── test-users.csv ← data-driven input
└── schemas/
└── user-schema.json ← shared match schema
Files in the same package as the feature file are referenced by bare name ('create-user-request.json'). Files in other packages use classpath: ('classpath:schemas/user-schema.json').
⚠️ Common mistakes
- Expecting CSV numbers to be numeric. Every field read from a CSV is a string.
users[0].idis"1", not1. Amatch response.id == users[0].idthat compares a numeric ID against the string"1"will fail. Either cast the CSV value (parseInt(users[0].id)) or use fuzzy matching (#number) when the exact value doesn't matter. - Using a relative path to reach a file in a different package.
'../common/helpers.js'looks logical but is not supported — Karate does not resolve..in file paths. For cross-package files, always useclasspath:common/helpers.js. Relative paths only work within the same directory as the feature file. - Modifying an object loaded from a file and expecting the file to update.
read()loads and parses the file into a JavaScript object in memory. Changing properties on that object (userData.name = 'Bob') does not write back to the file — the change exists only in the current scenario's scope. This is usually the right behaviour; just don't expect persistence.
🎯 Practice task
Move test data out of a feature file and into separate files. 35–45 minutes.
- Create
src/test/java/users/create-user-request.jsonwith a user body containingname,email, androle. Write a scenario that doesAnd request read('create-user-request.json'), POSTs to JSONPlaceholder/users, and assertsstatus 201. Run green. - Add
#(name)and#(email)markers to the JSON file. In the feature file, define* def name = 'Alice'and* def email = 'alice@test.com'before theread(). Confirm Karate substitutes the values correctly by checking the HTML report's request body. - Create
src/test/java/users/test-users.csvwith five rows (name, email, role). Load it with* def users = read('test-users.csv'). Assertkarate.sizeOf(users) == 5andusers[0].name == 'Alice'(use your actual first row). Confirm the loaded values are strings. - Create
src/test/java/common/helpers.jswith agenerateEmail(prefix)function. Load it in the feature file and callhelpers.generateEmail('test'). Print the result and confirm it's a unique email address. - Create
src/test/java/schemas/user-schema.jsonusing fuzzy markers for a typical user response. UseAnd match response == read('classpath:schemas/user-schema.json')in a GET scenario. Confirm the assertion passes. - Stretch: write a
create-user-expected.jsonthat matches the shape of JSONPlaceholder's POST response (id, name, email, username, etc. — use#numberand#stringmarkers). Load it in thematchstep after a POST. This is the file-based schema validation pattern you'll use in real projects.
Next lesson: data-driven testing with Scenario Outline, Examples tables, and CSV-backed test data — one scenario, many input rows.