# How to Handle Null Values and Specify Separators for Collection Properties in OWNER

> Learn how to handle null values and specify separators for OWNER collection properties. Customize default behaviors with annotations like @DefaultValue and @Separator.

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

---

**OWNER treats missing configuration keys as `null` for reference types (or throws `NullPointerException` for primitives) and tokenizes collection values on commas by default, but you can customize both behaviors using `@DefaultValue`, `@Separator`, and `@TokenizerClass` annotations.**

The OWNER library (matteobaccan/owner) maps Java properties files to typed interfaces, but handling missing values and parsing list-based configurations requires understanding its specific rules for **null handling** and **collection tokenization**. This guide explains exactly how the framework resolves undefined properties and how to control delimiter logic for array or collection return types.

## Understanding Null Handling in OWNER

OWNER follows predictable rules when a property is missing, empty, or explicitly set to `null`. These rules differ based on whether the return type is a primitive, wrapper, or collection.

### Undefined Properties and Default Behavior

When a property **is not defined** in any source and the interface method **lacks** a `@DefaultValue` annotation, OWNER returns `null` for reference types. However, for primitive return types (e.g., `int`, `boolean`), the framework throws a `NullPointerException` because primitives cannot hold null values.

According to the source code in [`PropertiesInvocationHandler.java`](https://github.com/matteobaccan/owner/blob/main/PropertiesInvocationHandler.java) (lines 77-87), the `resolveProperty` method returns `null` when no value is found, which then triggers the exception during auto-unboxing. To prevent this, declare a sensible default:

```java
public interface ServerConfig extends Config {
    @DefaultValue("8080")
    int port();  // Returns 8080 instead of throwing NPE
    
    @DefaultValue("localhost")
    String host();  // Returns "localhost" instead of null
}

```

### Empty Values vs. Null Literals

An **empty string** (`""`) behaves differently depending on the return type. For collections and arrays, OWNER interprets an empty value as an **empty collection** (zero elements), not `null`. For scalar types, the empty string passes through the converter chain, typically resulting in `null` or a conversion error.

The literal string `"null"` is treated as the text value `"null"`, not the Java `null` reference. If you need to explicitly remove a property at runtime, use `ConfigFactory.setProperty(key, null)` or `ConfigFactory.clearProperty(key)`, which removes the entry from the underlying `Properties` object entirely.

## Customizing Collection Separators

By default, OWNER tokenizes collection properties using a **comma** (`,`) delimiter. You can override this using two mechanisms: the `@Separator` annotation or a custom tokenizer class.

### Using the @Separator Annotation

The `@Separator` annotation, defined in [`Config.java`](https://github.com/matteobaccan/owner/blob/main/Config.java) (lines 312-317), specifies a delimiter string passed directly to `String.split`. You can apply it at the **method level** for specific properties or at the **interface level** as a default for all methods in that config.

The `TokenizerResolver` (lines 59-71) processes this annotation by instantiating a `SplitAndTrimTokenizer`, which splits the value and trims surrounding whitespace:

```java
@Separator(";")
@DefaultValue("red; green; blue")
List<String> colors();

// Class-level default with method override
@Separator(",")
interface CsvConfig extends Config {
    List<Integer> numbers();  // Uses comma
    
    @Separator("|")
    List<String> pipes();     // Uses pipe, overriding class default
}

```

### Implementing Custom Tokenizers

For complex parsing requirements (regex patterns, multi-character delimiters, or conditional logic), implement the `Tokenizer` interface and reference your class with `@TokenizerClass`:

```java
public class DashTokenizer implements Tokenizer {
    @Override
    public String[] tokens(String values) {
        return Arrays.stream(values.split("-", -1))
                     .map(String::trim)
                     .toArray(String[]::new);
    }
}

// Usage
@TokenizerClass(DashTokenizer.class)
@DefaultValue("one-two-three")
String[] dashSeparated();

```

### Tokenizer Resolution Rules

The `TokenizerResolver` enforces a strict mutual exclusivity rule: **you cannot use `@Separator` and `@TokenizerClass` on the same level**. Attempting this throws an `UnsupportedOperationException` (see [`TokenizerResolver.java`](https://github.com/matteobaccan/owner/blob/main/TokenizerResolver.java), lines 63-66). The resolver checks for `@TokenizerClass` first; if absent, it looks for `@Separator`; if neither exists, it defaults to the comma-based `SplitAndTrimTokenizer`.

The resolved tokenizer feeds into the `COLLECTION` converter in [`Converters.java`](https://github.com/matteobaccan/owner/blob/main/Converters.java) (lines 66-76), which first converts tokens via the `ARRAY` converter (lines 40-63) and then builds the requested collection type (`ArrayList`, `LinkedHashSet`, etc.).

## Practical Implementation Examples

This complete example demonstrates null safety, custom separators, and tokenizer implementation:

```java
public interface ApplicationConfig extends Config {
    // Returns empty list if property is "", returns null if missing
    @Separator(";")
    @DefaultValue("debug;info;warn")
    List<String> logLevels();
    
    // Uses custom tokenizer, never null due to default
    @TokenizerClass(DashTokenizer.class)
    @DefaultValue("dev-test-prod")
    String[] environments();
    
    // Primitive with default prevents NullPointerException
    @DefaultValue("30")
    int timeoutSeconds();
}

// Custom tokenizer implementation
public class DashTokenizer implements Tokenizer {
    @Override
    public String[] tokens(String input) {
        if (input == null || input.isEmpty()) {
            return new String[0];
        }
        return Arrays.stream(input.split("-"))
                     .map(String::trim)
                     .toArray(String[]::new);
    }
}

// Runtime usage
ApplicationConfig cfg = ConfigFactory.create(ApplicationConfig.class);
List<String> levels = cfg.logLevels();  // [debug, info, warn]

```

## Summary

- **Missing properties** return `null` for objects but throw `NullPointerException` for primitives unless you specify `@DefaultValue`.
- **Empty strings** yield empty collections for list/array types, not `null`.
- **Default separator** is a comma; override with `@Separator` or custom `Tokenizer` classes.
- **Mutual exclusivity**: `@Separator` and `@TokenizerClass` cannot coexist on the same method or interface.
- **Source locations**: Logic resides in [`PropertiesInvocationHandler.java`](https://github.com/matteobaccan/owner/blob/main/PropertiesInvocationHandler.java) (null handling), [`TokenizerResolver.java`](https://github.com/matteobaccan/owner/blob/main/TokenizerResolver.java) (separator resolution), and [`Converters.java`](https://github.com/matteobaccan/owner/blob/main/Converters.java) (collection building).

## Frequently Asked Questions

### What happens if a collection property is missing in OWNER?

If the property is undefined and has no `@DefaultValue`, the method returns `null` exactly like scalar properties. If you need an empty collection instead, annotate the method with `@DefaultValue("")` or provide a specific default list.

### Can I use regex as a separator in OWNER?

The built-in `@Separator` uses literal string delimiters passed to `String.split`, which interprets the argument as a regex. For complex regex patterns or custom parsing logic, implement a `Tokenizer` class and annotate the method with `@TokenizerClass(YourTokenizer.class)`.

### Why does OWNER throw NullPointerException for primitive return types?

When a property is missing, `PropertiesInvocationHandler.resolveProperty` (lines 77-87) returns `null`. Auto-unboxing this null into a primitive type (e.g., `int`, `double`) triggers a `NullPointerException`. Use wrapper types (`Integer`, `Double`) or add `@DefaultValue` to prevent this.

### How do I make a collection property return null instead of an empty list?

By default, an empty property value (`""`) produces an empty collection, not `null`. To distinguish between "empty" and "missing," check for the empty string in a custom `Tokenizer` or return `Optional<List<T>>` from your config method and map empty lists to `Optional.empty()` in your application code.