Skip to content

intuit/json2jsontransformer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

json2jsontransformer

License Java

A lightweight, config-driven Java library for transforming one JSON structure into another using JSONPath expressions and pluggable custom transformation logic.

No code changes are needed to add new field mappings — everything is driven by simple JSON config files.

Features

  • 🚀 Zero code changes - Add new transformations via JSON config files
  • 📝 JSONPath-based - Powerful path expressions for flexible field extraction
  • 🔌 Pluggable transformers - Extend with custom transformation logic
  • 🎯 Multiple config types - Support different input formats with separate configs
  • Lightweight - Minimal dependencies, fast execution
  • 🛡️ Type-safe - Supports string, array, object, boolean, and integer types
  • 🔄 Fallback paths - Graceful handling of missing fields
  • 📦 Flexible deployment - Classpath or file-system config loading

Table of contents


Prerequisites

  • Java 11 or higher (tested with Java 11, 17, and 21)
  • One of the following build tools:
    • Maven 3.6+ (also required for building from source)
    • Gradle 7.0+

Note: Ensure JAVA_HOME points to a Java 11+ JDK before running Maven. On macOS with multiple JDK installations:

export JAVA_HOME=$(/usr/libexec/java_home -v 11)

Installation

Maven

Add the following dependency to your pom.xml:

<dependency>
    <groupId>com.intuit.json2jsontransformer</groupId>
    <artifactId>json2jsontransformer</artifactId>
    <version>1.0.0</version>
</dependency>

Note: This adds the regular JAR which requires transitive dependencies. If you prefer a single JAR with all dependencies included (fat JAR), see the Building the JAR section.

Gradle

Add the following to your build.gradle:

dependencies {
    implementation 'com.intuit.json2jsontransformer:json2jsontransformer:1.0.0'
}

Or for Kotlin DSL (build.gradle.kts):

dependencies {
    implementation("com.intuit.json2jsontransformer:json2jsontransformer:1.0.0")
}

Manual Installation

If the library is not yet published to Maven Central, you can build it locally:

git clone https://github.com/intuit/json2jsontransformer.git
cd json2jsontransformer
mvn clean install -DskipTests

install (not package) is required — it copies the JAR into your local ~/.m2 repository so other Maven projects on the same machine can resolve the dependency. Then add the coordinates to your consumer project's pom.xml as shown in the Maven section above.


How it works

src/main/resources/
  config/
    <configType>/          ← one subdirectory per config type
      *.json               ← one or more JSON config files

At startup, JsonSchemaLoader scans every subdirectory under config/ and registers it as a named config type. At runtime you call transformJson(inputJson, "configType") to apply those mappings.

Each config file declares a list of output fields. For every field you specify:

  • which JSONPath(s) to read from the input
  • the output type (string, array, object, boolean, integer)
  • optional fallback paths tried when the primary path is absent
  • an optional default value used when everything else returns null
  • an optional custom transformation class for complex logic

Quickstart

1. Add the dependency (see Installation above)

2. Create a config file at src/main/resources/config/default/my_config.json:

{
  "parser_configs": [
    {
      "field_name": "userId",
      "is_mandatory": true,
      "target_schema": {
        "type": "string",
        "mappings": [{ "json_paths": ["$.user.id"] }]
      }
    },
    {
      "field_name": "email",
      "is_mandatory": false,
      "target_schema": {
        "type": "string",
        "mappings": [{ "json_paths": ["$.user.email"] }]
      }
    }
  ]
}

3. Transform JSON in code:

JsonSchemaLoader declares checked exceptions — handle them in your method:

// Option A — propagate (suits main methods and tests)
public static void main(String[] args) throws Exception {
    JsonTransformer transformer = new JsonTransformer(new JsonSchemaLoader());
    String inputJson = "{\"user\": {\"id\": \"u-001\", \"email\": \"alex@example.com\"}}";
    String outputJson = transformer.transformJson(inputJson, "default");
    // → {"email":"alex@example.com","userId":"u-001"}
}

// Option B — catch explicitly
try {
    JsonTransformer transformer = new JsonTransformer(new JsonSchemaLoader());
    String inputJson = "{\"user\": {\"id\": \"u-001\", \"email\": \"alex@example.com\"}}";
    String outputJson = transformer.transformJson(inputJson, "default");
} catch (URISyntaxException | IOException e) {
    // config loading failed or input JSON is malformed
}

Providing your own config (library consumers)

The library JAR ships with no bundled configs. You supply the config files that describe your own field mappings. There are three ways to do this.

Exception handling: All JsonSchemaLoader constructors declare throws URISyntaxException, IOException. Wrap instantiation in a try-catch or add the exceptions to your method signature (see the Error handling section).

Option 1 — Default classpath convention (recommended)

Place your config files under src/main/resources/config/<configType>/ in your project. JsonSchemaLoader scans config/ on the classpath automatically:

my-project/
  src/main/resources/
    config/
      orders/
        order_config.json    ← configType "orders"
      users/
        user_config.json     ← configType "users"
JsonSchemaLoader loader = new JsonSchemaLoader();          // scans classpath "config/"
JsonTransformer  transformer = new JsonTransformer(loader);

String output = transformer.transformJson(inputJson, "orders");

Option 2 — Custom classpath path

If you want to keep your configs under a different classpath directory (e.g. to avoid collisions with other libraries):

src/main/resources/
  my-app/configs/
    payments/
      payment_config.json
JsonSchemaLoader loader = new JsonSchemaLoader("my-app/configs/");
JsonTransformer  transformer = new JsonTransformer(loader);

String output = transformer.transformJson(inputJson, "payments");

Option 3 — Absolute file-system path

Useful when configs are managed outside the JAR — for example in a Docker volume, Kubernetes ConfigMap mount, or a shared config-management directory:

Path configDir = Paths.get("/etc/my-app/configs");
// or: Path configDir = Paths.get(System.getenv("CONFIG_DIR"));

JsonSchemaLoader loader = new JsonSchemaLoader(configDir);
JsonTransformer  transformer = new JsonTransformer(loader);

String output = transformer.transformJson(inputJson, "orders");

The directory must contain immediate subdirectories whose names become the config-type strings:

/etc/my-app/configs/
  orders/
    order_config.json     ← configType "orders"
  users/
    user_config.json      ← configType "users"

Config format reference

Top-level structure

{
  "parser_configs": [ ... ]
}

Each entry in parser_configs maps to one field in the output JSON.

{
  "field_name":    "outputFieldName",
  "is_mandatory":  true,
  "target_schema": { ... }
}
Field Type Description
field_name string Name of the output field
is_mandatory boolean Throw MissingFieldsException if no value can be resolved
target_schema object How to extract and transform the value

Schema types

string

Extracts a scalar value. Multiple json_paths are concatenated with a separator (default " "). You can customize the separator using sys_args:

{
  "field_name": "fullName",
  "target_schema": {
    "type": "string",
    "mappings": [
      { 
        "json_paths": ["$.firstName", "$.lastName"],
        "sys_args": { "separator": ", " }
      }
    ]
  }
}

Without custom separator (uses default " "):

{
  "field_name": "fullName",
  "target_schema": {
    "type": "string",
    "mappings": [
      { "json_paths": ["$.firstName", "$.lastName"] }
    ]
  }
}

array

Extracts multiple values and optionally organises them into objects using target_field.

{
  "field_name": "suggestions",
  "target_schema": {
    "type": "array",
    "mappings": [
      { "target_field": "label", "json_paths": ["$.items[*].label"] },
      { "target_field": "value", "json_paths": ["$.items[*].value"] }
    ]
  }
}

Output: [{"label":"Option A","value":"val-a"}, ...]

object

Builds a nested object. Each mapping contributes a field to the result object via target_field.

{
  "field_name": "metadata",
  "target_schema": {
    "type": "object",
    "mappings": [
      { "target_field": "source", "json_paths": ["$.meta.src"] },
      { "target_field": "version", "json_paths": ["$.meta.ver"] }
    ]
  }
}

boolean

Reads true / false (as boolean or string).

{
  "field_name": "isActive",
  "target_schema": {
    "type": "boolean",
    "default_value": "false",
    "mappings": [{ "json_paths": ["$.active"] }]
  }
}

integer

Reads an integer (also handles numeric strings like "404").

{
  "field_name": "statusCode",
  "target_schema": {
    "type": "integer",
    "mappings": [{ "json_paths": ["$.status"] }]
  }
}

Fallbacks

When the primary path is absent, fallbacks are tried in order.

{
  "field_name": "eventId",
  "target_schema": {
    "type": "string",
    "mappings": [
      {
        "json_paths": ["$.eventId"],
        "fallbacks": [
          { "json_paths": ["$.id"] },
          { "json_paths": ["$.transactionId"] }
        ]
      }
    ]
  }
}

Default values

Returned when all paths and fallbacks resolve to null.

{
  "field_name": "messageType",
  "target_schema": {
    "type": "string",
    "default_value": "text",
    "mappings": [{ "json_paths": ["$.messageType"] }]
  }
}

For object type, default_value must be a JSON string:

"default_value": "{\"source\":\"unknown\",\"version\":\"v0\"}"

Custom transformers

Reference a custom transformer class by its fully-qualified name in transformation_class.

{
  "field_name": "normalizedCurrency",
  "target_schema": {
    "type": "string",
    "mappings": [
      {
        "json_paths": ["$.currencyCode"],
        "transformation_class": "com.intuit.json2jsontransformer.transform.UpperCaseTransformer"
      }
    ]
  }
}

Extra parameters can be passed via sys_args:

{
  "json_paths": ["$.firstName", "$.lastName"],
  "sys_args": { "separator": ", " }
}

Nested target fields

Use dot notation in target_field to write into nested objects.

{
  "target_field": "address.city",
  "json_paths": ["$.location.city"]
}

Output: {"address": {"city": "London"}}


Custom transformer guide

Implement the Transformation interface:

public interface Transformation {
    Object transformWithPaths(DocumentContext context, List<String> jsonPaths, Map<String, Object> sysArgs);
}

Example: UpperCaseTransformer

package com.example.transform;

import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.PathNotFoundException;
import com.intuit.json2jsontransformer.transform.Transformation;

import java.util.List;
import java.util.Map;

public class UpperCaseTransformer implements Transformation {

    @Override
    public Object transformWithPaths(DocumentContext context, List<String> jsonPaths, Map<String, Object> sysArgs) {
        for (String path : jsonPaths) {
            try {
                Object value = context.read(path);
                if (value != null) {
                    return value.toString().toUpperCase();
                }
            } catch (PathNotFoundException ignored) {}
        }
        return null;
    }
}

Reference it in the config:

{
  "json_paths": ["$.currencyCode"],
  "transformation_class": "com.example.transform.UpperCaseTransformer"
}

Tip: Transformers are instantiated once via their no-arg constructor and cached. Keep them stateless.


Multi-config example: normalising two webhook formats

A common real-world need: two external services send events in completely different JSON shapes, but you want a single normalised output schema.

Directory layout

config/
  provider-alpha/
    event_config.json
  provider-beta/
    event_config.json

Provider Alpha event (input)

{
  "id": "txn-alpha-999",
  "event": "transaction.completed",
  "payload": {
    "customerId": "cust-alpha-111",
    "amount": 4999,
    "currencyCode": "usd",
    "meta": { "orderId": "order-alpha-777" }
  }
}

Provider Beta event (input)

{
  "eventType": "payment.success",
  "data": {
    "transactionId": "txn-beta-888",
    "buyer": { "id": "cust-beta-222" },
    "total": { "value": 2599, "currency": "eur" },
    "externalRef": "order-beta-555"
  }
}

Normalised output (both produce the same shape)

{
  "eventId":    "txn-alpha-999",
  "eventType":  "transaction.completed",
  "customerId": "cust-alpha-111",
  "amount":     4999,
  "currency":   "USD",
  "orderId":    "order-alpha-777"
}

Config for Provider Alpha (config/provider-alpha/event_config.json)

{
  "parser_configs": [
    { "field_name": "eventId",    "is_mandatory": true,  "target_schema": { "type": "string",  "mappings": [{ "json_paths": ["$.id"] }] } },
    { "field_name": "eventType",  "is_mandatory": true,  "target_schema": { "type": "string",  "mappings": [{ "json_paths": ["$.event"] }] } },
    { "field_name": "customerId", "is_mandatory": false, "target_schema": { "type": "string",  "mappings": [{ "json_paths": ["$.payload.customerId"] }] } },
    { "field_name": "amount",     "is_mandatory": false, "target_schema": { "type": "integer", "mappings": [{ "json_paths": ["$.payload.amount"] }] } },
    { "field_name": "currency",   "is_mandatory": false, "target_schema": { "type": "string",  "mappings": [{ "json_paths": ["$.payload.currencyCode"], "transformation_class": "com.intuit.json2jsontransformer.transform.UpperCaseTransformer" }] } },
    { "field_name": "orderId",    "is_mandatory": false, "target_schema": { "type": "string",  "mappings": [{ "json_paths": ["$.payload.meta.orderId"] }] } }
  ]
}

Config for Provider Beta (config/provider-beta/event_config.json)

{
  "parser_configs": [
    { "field_name": "eventId",    "is_mandatory": true,  "target_schema": { "type": "string",  "mappings": [{ "json_paths": ["$.data.transactionId"] }] } },
    { "field_name": "eventType",  "is_mandatory": true,  "target_schema": { "type": "string",  "mappings": [{ "json_paths": ["$.eventType"] }] } },
    { "field_name": "customerId", "is_mandatory": false, "target_schema": { "type": "string",  "mappings": [{ "json_paths": ["$.data.buyer.id"] }] } },
    { "field_name": "amount",     "is_mandatory": false, "target_schema": { "type": "integer", "mappings": [{ "json_paths": ["$.data.total.value"] }] } },
    { "field_name": "currency",   "is_mandatory": false, "target_schema": { "type": "string",  "mappings": [{ "json_paths": ["$.data.total.currency"], "transformation_class": "com.intuit.json2jsontransformer.transform.UpperCaseTransformer" }] } },
    { "field_name": "orderId",    "is_mandatory": false, "target_schema": { "type": "string",  "mappings": [{ "json_paths": ["$.data.externalRef"] }] } }
  ]
}

Java usage

// JsonSchemaLoader throws URISyntaxException, IOException — propagate or catch
JsonTransformer transformer = new JsonTransformer(new JsonSchemaLoader());

// Route each event to its config type
String configType = determineProvider(incomingEvent); // "provider-alpha" or "provider-beta"
String normalised = transformer.transformJson(incomingEvent, configType);

Error handling

The library throws several exceptions that you should handle in your code:

MissingFieldsException

Thrown when one or more fields marked is_mandatory: true in the config cannot be resolved from the input JSON.

try {
    String output = transformer.transformJson(inputJson, "default");
} catch (MissingFieldsException e) {
    System.err.println("Missing mandatory fields: " + e.getFields());
    // e.getFields()     → formatted String,  e.g. "[ eventId, sessionId ]"
    // e.getFieldList()  → List<String> of individual field names for programmatic use
}

Note: All fields are evaluated before the exception is thrown. A single call reports every missing mandatory field at once — not just the first one encountered.

IOException

Thrown when the input JSON string is malformed or cannot be parsed.

try {
    String output = transformer.transformJson(inputJson, "default");
} catch (IOException e) {
    System.err.println("Invalid JSON input: " + e.getMessage());
}

IllegalArgumentException

Thrown when the specified configType doesn't exist or wasn't loaded.

try {
    String output = transformer.transformJson(inputJson, "nonexistent-config");
} catch (IllegalArgumentException e) {
    System.err.println("Config type not found: " + e.getMessage());
    // Message includes available config types
}

URISyntaxException / IOException (from JsonSchemaLoader)

Thrown during initialization when config files cannot be loaded:

try {
    JsonSchemaLoader loader = new JsonSchemaLoader();
    JsonTransformer transformer = new JsonTransformer(loader);
} catch (URISyntaxException | IOException e) {
    System.err.println("Failed to load configs: " + e.getMessage());
}

Complete error handling example

public String transformSafely(String inputJson, String configType) {
    try {
        JsonSchemaLoader loader = new JsonSchemaLoader();
        JsonTransformer transformer = new JsonTransformer(loader);
        return transformer.transformJson(inputJson, configType);
    } catch (MissingFieldsException e) {
        log.error("Missing mandatory fields: {}", e.getFields());
        throw new BusinessException("Required fields missing", e);
    } catch (IllegalArgumentException e) {
        log.error("Invalid config type: {}", configType);
        throw new BusinessException("Config type not found", e);
    } catch (IOException e) {
        log.error("Invalid JSON input", e);
        throw new BusinessException("Malformed JSON", e);
    } catch (URISyntaxException e) {
        log.error("Failed to initialize transformer", e);
        throw new BusinessException("Initialization failed", e);
    }
}

Thread safety

JsonTransformer and JsonSchemaLoader are thread-safe for concurrent read operations:

  • Safe: Multiple threads can call transformJson() concurrently with the same JsonTransformer instance
  • Safe: Configs are loaded once at initialization and cached
  • Safe: Custom transformers are instantiated once and cached (must be stateless)

Important: Custom Transformation implementations must be stateless since they are cached and reused across threads. Do not store instance variables that could cause race conditions.


Troubleshooting

Config files not found

Problem: JsonSchemaLoader warns "No config directory found on classpath"

Solutions:

  1. Ensure config files are in src/main/resources/config/<configType>/
  2. Verify files are included in the JAR (check target/classes/ after build)
  3. Use explicit path: new JsonSchemaLoader("my-custom-path/")
  4. Use file-system path: new JsonSchemaLoader(Paths.get("/path/to/configs"))

ConfigType not found

Problem: IllegalArgumentException: No config loaded for configType='xyz'

Solutions:

  1. Check the directory name matches exactly (case-sensitive)
  2. Verify at least one .json file exists in that directory
  3. List available configs: transformer.getConfigsMap().keySet()

Custom transformer class not found

Problem: Transformer class cannot be instantiated

Solutions:

  1. Ensure the class implements Transformation interface
  2. Verify fully-qualified class name in config matches exactly
  3. Check the class is on the classpath
  4. Ensure the class has a no-arg constructor
  5. Check logs for specific error messages

JSONPath not working

Problem: Fields are not being extracted

Solutions:

  1. Test JSONPath expressions using JSONPath Evaluator
  2. Verify the input JSON structure matches your paths
  3. Check for typos in field names (case-sensitive)
  4. Use fallback paths for optional fields
  5. Enable debug logging to see what's being extracted

Malformed JSON

Problem: IOException when parsing input

Solutions:

  1. Validate input JSON using a JSON validator
  2. Check for unescaped control characters (library allows them, but verify)
  3. Ensure proper encoding (UTF-8)

Verbose DEBUG logging from JSONPath

Problem: com.jayway.jsonpath.internal.path.CompiledPath emits a DEBUG line for every path evaluation, flooding application logs.

Solution: Suppress it in your logback.xml (or equivalent):

<logger name="com.jayway.jsonpath" level="WARN"/>

Building the JAR

The project supports building two types of JAR files:

Regular JAR (Default)

The regular JAR contains only the library classes and requires clients to add transitive dependencies.

Build command:

mvn clean package

This creates: target/json2jsontransformer-1.0.0.jar

When to use:

  • Maven/Gradle projects that can manage dependencies
  • When you want to control dependency versions
  • Smaller JAR size (~33KB)

Required dependencies (if using regular JAR):

  • com.fasterxml.jackson.core:jackson-databind:2.17.2
  • com.jayway.jsonpath:json-path:2.9.0
  • net.minidev:json-smart:2.5.2
  • org.apache.commons:commons-lang3:3.14.0
  • commons-collections:commons-collections:3.2.2
  • org.slf4j:slf4j-api:2.0.13

Fat JAR (Uber JAR)

The fat JAR includes all transitive dependencies bundled, so clients don't need to add them separately.

Build command:

mvn clean package

This creates both:

  • target/json2jsontransformer-1.0.0.jar (regular JAR)
  • target/json2jsontransformer-1.0.0-fat.jar (fat JAR with all dependencies)

When to use:

  • Standalone applications
  • Non-Maven/Gradle projects
  • Simple deployment scenarios
  • When you want a single JAR file

Note: The fat JAR is larger (~4MB) but includes everything needed to run. The regular JAR is only ~33KB but requires clients to add transitive dependencies.

Build Options

Skip tests:

mvn clean package -DskipTests

Build only regular JAR (skip fat JAR):

mvn clean package -Dmaven.shade.skip=true

Running the tests

mvn test

Tests cover all five schema types (string, array, object, boolean, integer), fallback paths, default values, mandatory field enforcement, custom transformers, and multi-config routing.


Contributing

Contributions are welcome! Please follow these guidelines:

  1. Fork the repository and create a feature branch
  2. Write tests for new functionality
  3. Follow existing code style and conventions
  4. Update documentation as needed
  5. Submit a pull request with a clear description

Development setup

# Clone the repository
git clone https://github.com/intuit/json2jsontransformer.git
cd json2jsontransformer

# Build and run tests
mvn clean test

# Build JARs
mvn clean package

Reporting issues

Please report bugs or request features by opening an issue on GitHub. Include:

  • Library version
  • Java version
  • Steps to reproduce
  • Expected vs actual behavior
  • Relevant code/config examples

License

This project is licensed under the Apache License 2.0.


Links

About

No description, website, or topics provided.

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages