# How to Implement a Preprocessor to Transform Property Values Before Use in Owner

> Learn how to implement a Preprocessor in OWNER to transform property values before type conversion. Intercept and modify raw strings for custom value manipulation.

- Repository: [Matteo Baccan/owner](https://github.com/matteobaccan/owner)
- Tags: how-to-guide
- Published: 2026-03-07

---

**Implement the `Preprocessor` interface and register your class with the `@PreprocessorClasses` annotation to intercept and modify raw property strings before type conversion occurs.**

The Owner configuration library (matteobaccan/owner) provides a powerful extension point that lets you sanitize, normalize, or decrypt property values immediately after they are read from the source but before they are converted to target types. By implementing a Preprocessor to transform property values before use, you can enforce consistent formatting across your entire configuration layer without changing the underlying property files.

## Understanding the Preprocessor Pipeline

The transformation mechanism relies on four core components working in sequence. Understanding these files helps you debug and extend the behavior effectively.

### Core Components

| Component | File Path | Purpose |
|-----------|-----------|---------|
| `Preprocessor` interface | [`owner/src/main/java/org/aeonbits/owner/Preprocessor.java`](https://github.com/matteobaccan/owner/blob/main/owner/src/main/java/org/aeonbits/owner/Preprocessor.java) | Defines the contract with a single `process(String)` method that receives the raw value and returns the transformed string. |
| `@PreprocessorClasses` annotation | [`owner/src/main/java/org/aeonbits/owner/Config.java`](https://github.com/matteobaccan/owner/blob/main/owner/src/main/java/org/aeonbits/owner/Config.java) (lines 58-68) | Declares which preprocessor classes apply to a method or an entire config interface. |
| `PreprocessorResolver` | [`owner/src/main/java/org/aeonbits/owner/PreprocessorResolver.java`](https://github.com/matteobaccan/owner/blob/main/owner/src/main/java/org/aeonbits/owner/PreprocessorResolver.java) | Instantiates and orders the processors by reading annotations from the method first, then the declaring interface. |
| `PropertiesInvocationHandler` | [`owner/src/main/java/org/aeonbits/owner/PropertiesInvocationHandler.java`](https://github.com/matteobaccan/owner/blob/main/owner/src/main/java/org/aeonbits/owner/PropertiesInvocationHandler.java) (lines 97-103) | Executes the `preProcess` method, feeding the raw property value through each resolved processor sequentially. |

### Execution Flow

When you invoke a method on your config interface, Owner executes these steps:

1. **Read** the raw property string from the underlying source (file, system property, environment variable).
2. **Resolve** the list of preprocessors via `PreprocessorResolver.resolvePreprocessors`.
3. **Transform** the value by calling `process(String)` on each preprocessor in order.
4. **Convert** the final string to the declared return type (int, boolean, custom object, etc.).

Because preprocessing occurs *before* type conversion, you can modify the raw text without worrying about format exceptions, provided your preprocessor outputs a string compatible with the target converter.

## Implementing a Custom Preprocessor to Transform Property Values

Creating your own preprocessor requires implementing the interface and registering it via annotations. Here is the complete workflow.

### Step 1: Create the Preprocessor Class

Implement the `org.aeonbits.owner.Preprocessor` interface and override the `process` method. This example normalizes whitespace by collapsing multiple spaces and trimming the result.

```java
package com.example.owner;

import org.aeonbits.owner.Preprocessor;

/**
 * Normalizes whitespace in property values.
 */
public class WhitespaceNormalizer implements Preprocessor {
    @Override
    public String process(String input) {
        if (input == null) {
            return null;
        }
        return input.replaceAll("\\s+", " ").trim();
    }
}

```

### Step 2: Register with @PreprocessorClasses

Apply the `@PreprocessorClasses` annotation to your config interface or to individual methods. This example applies the preprocessor to the entire interface, affecting every property retrieval.

```java
package com.example.owner;

import org.aeonbits.owner.Config;
import org.aeonbits.owner.Config.PreprocessorClasses;

@PreprocessorClasses({ WhitespaceNormalizer.class })
public interface ApplicationConfig extends Config {

    @DefaultValue("   admin   user   ")
    String adminUser();  // Returns "admin user"

    @DefaultValue("  127.0.0.1  ")
    String serverHost(); // Returns "127.0.0.1"
}

```

### Step 3: Method-Level vs Interface-Level Registration

You can scope preprocessors narrowly to specific methods or broadly to the entire configuration interface. Method-level annotations override interface-level behavior for that specific property.

```java
@PreprocessorClasses({ GlobalPreprocessor.class }) // Applied to all methods
public interface MixedConfig extends Config {

    @PreprocessorClasses({ SpecificPreprocessor.class }) // Applied first
    @DefaultValue("value")
    String specificProperty(); // Processing order: SpecificPreprocessor → GlobalPreprocessor

    String normalProperty();   // Only GlobalPreprocessor applies
}

```

## Preprocessor Execution Order

When both method-level and interface-level `@PreprocessorClasses` annotations are present, Owner concatenates the lists in a specific sequence defined in `PreprocessorResolver.resolvePreprocessors`.

The execution order is:

1. **Method-level preprocessors** (declared directly on the invoked method)
2. **Interface-level preprocessors** (declared on the config interface)

This ordering ensures that specific, local transformations occur before broader, global ones. For example, if you have a method-specific decryption preprocessor and an interface-wide trim preprocessor, the decryption runs first, then the trimming occurs.

## Real-World Examples from the Owner Test Suite

The Owner library includes comprehensive tests demonstrating practical preprocessor implementations. The `PreprocessorTest` class in [`owner/src/test/java/org/aeonbits/owner/PreprocessorTest.java`](https://github.com/matteobaccan/owner/blob/main/owner/src/test/java/org/aeonbits/owner/PreprocessorTest.java) defines three useful examples as static inner classes:

- **`Trim`**: Removes leading and trailing whitespace from property values.
- **`ToLowerCase`**: Converts the entire string to lowercase for case-insensitive configuration handling.
- **`SkipInlineComments`**: Strips inline comments (text following `#` or `!`) commonly found in property files.

These examples demonstrate that preprocessors can handle everything from simple string manipulation to complex parsing logic before Owner attempts type conversion.

## Summary

Implementing a Preprocessor to transform property values before use in Owner involves these key steps:

- Implement the `org.aeonbits.owner.Preprocessor` interface and override the `process(String)` method to define your transformation logic.
- Register your preprocessor class using the `@PreprocessorClasses` annotation on either the config interface (global scope) or individual methods (local scope).
- Understand that method-level preprocessors execute before interface-level ones, allowing you to chain specific and global transformations.
- Reference the source files [`Preprocessor.java`](https://github.com/matteobaccan/owner/blob/main/Preprocessor.java), [`Config.java`](https://github.com/matteobaccan/owner/blob/main/Config.java), [`PreprocessorResolver.java`](https://github.com/matteobaccan/owner/blob/main/PreprocessorResolver.java), and [`PropertiesInvocationHandler.java`](https://github.com/matteobaccan/owner/blob/main/PropertiesInvocationHandler.java) to understand the internal execution pipeline.

## Frequently Asked Questions

### Can I apply multiple preprocessors to the same property?

Yes. The `@PreprocessorClasses` annotation accepts an array of classes, and they execute in the order declared. Additionally, you can combine method-level and interface-level annotations to create complex transformation chains. For example, you might declare `{ Trim.class, ToLowerCase.class }` to first remove whitespace and then convert to lowercase.

### What happens if my preprocessor returns null?

If your `process` method returns `null`, Owner passes that `null` value to the type converter. Depending on the target type, this may result in a `null` value for object types or a conversion exception for primitive types. Always ensure your preprocessor handles `null` inputs gracefully, typically by returning `null` unchanged or providing a default string representation.

### Can preprocessors access the property key or configuration metadata?

No. The `Preprocessor` interface only receives the raw property value as a `String`. It does not have access to the property key, the method being invoked, or other metadata. If you need context-aware transformations, consider implementing a custom `Converter` instead, which receives the `Method` object and can access annotations and other metadata.

### Are preprocessors instantiated once or on every property access?

Owner instantiates preprocessor classes once per configuration interface creation and caches the instances. The `PreprocessorResolver` creates the instances when resolving the preprocessor list, and these same instances are reused for all subsequent property retrievals on that config object. Therefore, preprocessors should be stateless or thread-safe if they maintain any internal state.