In Apex, while JSON.deserialize() is effective for mapping JSON to strongly-typed Apex objects, it requires a predefined class structure. For dynamic JSON or when you only need a few specific values from a large, complex structure, using JSON.deserializeUntyped() is the alternative. However, this often leads to verbose and hard-to-read code with multiple levels of casting and map lookups, a pattern often referred to as "map-string-object hell."
The apex-jpath library simplifies this by providing a concise, expressive way to query and extract data from a JSON structure using JPath expressions. This avoids the need for boilerplate code and makes the developer's intent much clearer.
- Overview
- Installation
- Features
- Usage Examples
- More Usage Examples
- How?
- Supported Operations
- Limitations and Design Choices
- Testing
- License
- Contributing
- Support
This library provides JSONPath functionality directly within Apex, allowing developers to query and filter JSON data using familiar JSONPath expressions. It's designed specifically for Salesforce environments and supports common JSONPath operations including:
- Property access (
.property) - Array indexing (
[0],[-1]) - Wildcard matching (
[*]) - Filter expressions (
[?(@.property > 10)]) - Recursive descent (
..property) - Slice operations (
[0:2])
Install this package using the following URL in your Salesforce prod org:
Install this package using the following URL in your Salesforce sandbox org:
- Clone this repository
- Deploy to your Salesforce org using sf:
sf force:source:deploy -p force-app
- Native Apex Implementation: Pure Apex code with no external dependencies
- Full JSONPath Support: Implements core JSONPath specification with Salesforce-specific enhancements
- Type Safety: Robust type handling with automatic conversion between numeric types
- Filter Expressions: Complex filtering with comparison operators and logical conditions
- Error Handling: Comprehensive exception handling with descriptive error messages
- Performance Optimized: Efficient parsing and evaluation of JSONPath expressions
Quick Testing:
For rapid hands-on evaluation, see the attached example files (examples/example1.txtandexamples/example2.txt).
These contain ready-to-copy Apex code blocks for Execute Anonymous windows, allowing you to quickly test JSONPath queries and verify library functionality in your Salesforce org.
Important: Run each test block individually to avoid hitting governor limits (see comments in the files for details).
String json = '{"name": "John", "age": 30}';
JSONPath jp = new JSONPath(json);
List<Object> result = jp.selectPath('$.name');
// Returns: ["John"]String json = '{"fruits": ["apple", "banana", "orange"]}';
JSONPath jp = new JSONPath(json);
List<Object> result = jp.selectPath('$.fruits[1]');
// Returns: ["banana"]String json = '{
"employees": [
{"name": "John", "salary": 50000},
{"name": "Jane", "salary": 60000},
{"name": "Bob", "salary": 45000}
]
}';
JSONPath jp = new JSONPath(json);
List<Object> result = jp.selectPath('$.employees[?(@.salary > 50000)]');
// Returns employees with salary > 50000String json = '{
"items": [
{"name": "A", "price": "12.99"},
{"name": "B", "price": 12.99},
{"name": "C", "price": 8}
]
}';
JSONPath jp = new JSONPath(json);
List<Object> result = jp.selectPath('$.items[?(@.price >= 12.99)]');
// Correctly matches both items with price >= 12.99Find a series of examples to get you started with apex-jpath. All examples will use the following sample JSON, which represents a simple data structure for a fictional online store.
{
"store": {
"name": "The Awesome Store",
"location": {
"city": "San Francisco",
"state": "CA"
},
"products": [
{
"id": 1,
"name": "Laptop",
"price": 1200,
"tags": ["electronics", "computers"],
"inStock": true
},
{
"id": 2,
"name": "Mouse",
"price": 25,
"tags": ["electronics", "accessories"],
"inStock": true
},
{
"id": 3,
"name": "Book",
"price": 15.50,
"tags": ["reading", "education"],
"inStock": false
}
]
}
}These examples cover the most common use cases for accessing data.
To get the name of the store.
Apex Code:
String jsonString = '...'; // Your JSON string here
JPath path = new JPath(jsonString);
String storeName = (String) path.select('store.name');
// Extracts the store's name: "The Awesome Store"To get the city where the store is located.
Apex Code:
String jsonString = '...'; // Your JSON string here
JPath path = new JPath(jsonString);
String city = (String) path.select('store.location.city');
// Extracts the city: "San Francisco"To get the first product in the products array. Note that the result will be a Map<String, Object> representing the JSON object.
Apex Code:
String jsonString = '...'; // Your JSON string here
JPath path = new JPath(jsonString);
Map<String, Object> firstProduct = (Map<String, Object>) path.select('store.products[0]');
// Retrieves the first product object from the array.
System.assertEquals(1, firstProduct.get('id'));
System.assertEquals('Laptop', firstProduct.get('name'));To get the name of the second product.
Apex Code:
String jsonString = '...'; // Your JSON string here
JPath path = new JPath(jsonString);
String secondProductName = (String) path.select('store.products[1].name');
// Gets the name of the second product: "Mouse"These examples show how to work with collections of data.
To get a list of all product names.
Apex Code:
String jsonString = '...'; // Your JSON string here
JPath path = new JPath(jsonString);
List<Object> productNames = (List<Object>) path.select('store.products[*].name');
// Returns a list of all product names: ["Laptop", "Mouse", "Book"]To get the full list of products.
Apex Code:
String jsonString = '...'; // Your JSON string here
JPath path = new JPath(jsonString);
List<Object> allProducts = (List<Object>) path.select('store.products');
// Returns the entire list of product objects.
System.assertEquals(3, allProducts.size());These examples demonstrate the real power of apex-jpath by using filters to query for specific data.
To find the product with the id of 3.
Apex Code:
String jsonString = '...'; // Your JSON string here
JPath path = new JPath(jsonString);
// Note that the filter returns a list of matching elements
List<Object> results = (List<Object>) path.select('store.products[?(@.id == 3)]');
// Finds the product object where the id is 3.
System.assertEquals(1, results.size());
Map<String, Object> book = (Map<String, Object>) results[0];
System.assertEquals('Book', book.get('name'));
System.assertEquals(15.50, book.get('price'));To get the price of the product named "Laptop".
Apex Code:
String jsonString = '...'; // Your JSON string here
JPath path = new JPath(jsonString);
List<Object> prices = (List<Object>) path.select('store.products[?(@.name == \'Laptop\')].price');
// Finds the price of the product named "Laptop": [1200]
Decimal laptopPrice = (Decimal) prices[0];
System.assertEquals(1, prices.size());
System.assertEquals(1200, laptopPrice);To get the names of all products that are inStock.
Apex Code:
String jsonString = '...';
JPath path = new JPath(jsonString);
List<Object> inStockProducts = (List<Object>) path.select('store.products[?(@.inStock == true)].name');
// Returns the names of all products that are in stock: ["Laptop", "Mouse"]
System.assertEquals(new List<Object>{'Laptop', 'Mouse'}, inStockProducts);To get the names of all products with a price greater than 30.
Apex Code:
String jsonString = '...';
JPath path = new JPath(jsonString);
List<Object> expensiveProducts = (List<Object>) path.select('store.products[?(@.price > 30)].name');
// Gets the names of products with a price greater than 30: ["Laptop"]
System.assertEquals(new List<Object>{'Laptop'}, expensiveProducts);- JSONPath: Main class for querying JSON data
- JSONPathException: Custom exception class for JSONPath-related errors
- JSONPathTest: Comprehensive test suite demonstrating usage
| Operation | Syntax | Description |
|---|---|---|
| Property Access | $.property |
Access object property |
| Array Indexing | $.array[0] |
Access array element by index |
| Wildcard | $.array[*] |
Select all elements |
| Filter | $.array[?(@.prop > 10)] |
Filter elements |
| Recursive Descent | $..property |
Find property at any level |
| Slice | $.array[0:2] |
Extract slice of array |
This library is intentionally focused on the most common and essential JSONPath features that are safe and performant on the Salesforce platform. This Apex version deliberately omits the following complex and platform-problematic features:
1. Script Expressions (...) and eval()
- What it is: The original JSONPath proposal allowed for arbitrary script expressions (typically JavaScript) to be evaluated within brackets, like
$.store.book[(@.length-1)]to get the last book, or$.store.book[?(@.price > 10 && @.category === 'fiction')]. - Why it's omitted:
- Security Risk: Executing arbitrary script expressions is equivalent to
eval(), which is a massive security vulnerability. It is explicitly disallowed in Apex for this reason. - Complexity: Building a safe and efficient expression parser and evaluator from scratch in Apex is a monumental task that would make the library incredibly large and slow.
- Security Risk: Executing arbitrary script expressions is equivalent to
2. Complex Filter Logic (&&, ||, Grouping)
- What it is: The ability to combine filter conditions, such as
[?(@.price < 10 && @.category == 'fiction')]. - Why it's omitted:
- Governor Limits: A full-fledged logical expression parser can become very CPU-intensive, especially with nested logic and large arrays, posing a risk to Salesforce governor limits.
- Pragmatism: While powerful, these queries can often be simplified. A developer can first filter by
[?(@.price < 10)]and then use a simple Apex loop on the smaller result set to check the second condition (category == 'fiction'). This approach is more explicit and often safer from a performance standpoint.
3. JSONPath Functions (.length(), .avg(), .match(), etc.)
- What it is: The IETF RFC standardises functions that can be used within expressions, like
[?(@.title.length() > 10)]. - Why it's omitted:
- Implementation Complexity: Each function would require custom logic to implement. Like complex filters, this adds significant overhead and potential performance issues. The logic for a function like
.match()(regex matching) would be particularly complex in Apex.
- Implementation Complexity: Each function would require custom logic to implement. Like complex filters, this adds significant overhead and potential performance issues. The logic for a function like
4. Accessing the Root ($) within Filters
- What it is: The ability to compare an item against a value from the root of the document, like
[?(@.price > $.expensive)]. - Why it's omitted: The current simple filter parser is not designed to handle expressions that break out of the current context (
@). Supporting this would require a more sophisticated evaluation engine that maintains a reference to the root node throughout the filter evaluation process.
By omitting features that rely on eval() or require highly complex, CPU-intensive parsing, the library remains secure and efficient, staying well within governor limits for all reasonable use cases. It provides a massive leap in functionality over parsing JSON manually with JSON.deserializeUntyped and nested loops, while avoiding the advanced edge cases that offer diminishing returns for their immense implementation cost and risk.
Run all tests using:
sf force:apex:test:run -t JSONPathTestMIT License
- Fork the repository
- Create a feature branch
- Commit your changes
- Push to the branch
- Create a Pull Request
Questions and feature requests? Please use the Discussions tab. For bugs, open an issue.