CodeGym /Courses /Spring Core /ConversionService: type conversion

ConversionService: type conversion

Spring Core
Level 13, Lesson 1
Available

1. The role of the shared ConversionService

We have already seen that it is better not to drag strings from properties into services: then parsing gets scattered across the project, and a config error shows up not at startup, but in the middle of a scenario. Now we need one shared mechanism that converts values centrally and by common rules. That is exactly the role taken by ConversionService.

For ContextFlow this is especially noticeable with NotificationChannel and ReportFormat. The notification service needs a real NotificationChannel, the report service needs a ReportFormat, not a String that still has to be trimmed and converted to uppercase. If every class parses the string on its own, configuration leaks back into business logic.

ConversionService: canConvert(...) and convert(...)

If you are afraid of the word “service” in the name (after a dozen *Service classes in a project, that is normal), then think of ConversionService as a “single type translator”. It is not about business logic, it is about container infrastructure: it does not care what you convert — a notification channel, numbers, enums, or something else. It simply handles the translation.

At the mental model level, a beginner only needs to remember two methods. The first is canConvert(...). It answers the question: “Can you convert from one type to another?” The second is convert(...): “Convert this value to the needed type.” What matters is that Spring tries to keep this mechanism centralized: instead of each class deciding on its own how to parse a string, we ask one shared translator.

A small example of programmatic use, just to “feel” the API with your hands, like a cat testing a package for durability:

package com.example.contextflow.support.conversion;

import org.springframework.core.convert.ConversionService;
import org.springframework.stereotype.Component;

@Component
public class ConversionProbe {
    // Infrastructure conversion service provided by Spring
    private final ConversionService conversionService;

    public ConversionProbe(ConversionService conversionService) {
        // Important: we do not create the converter ourselves, we take it from the context
        this.conversionService = conversionService;
    }

    public void demo() {
        // Convert a string to Integer through the shared mechanism
        Integer n = conversionService.convert("42", Integer.class);

        // The important part here is the fact of conversion through ConversionService
        System.out.println("n = " + n); // n = 42
    }
}

What matters here is not the number 42 itself (although the number 42 is, of course, special), but the fact that we do not write Integer.parseInt(...); we use a shared mechanism.

Now let us add canConvert(...) to see the “capability map”:

import org.springframework.core.convert.ConversionService;

// Quick check: can ConversionService convert String -> Integer?
boolean ok = conversionService.canConvert(String.class, Integer.class);
System.out.println("String -> Integer supported = " + ok); // true

And this true is the key point: if Spring can convert a string to a number, then @Value can pass int, Integer, long, and so on into a constructor without manual parsing code (provided the container uses ConversionService, which we will talk about next).

2. DefaultConversionService out of the box

It often seems that “conversion” is something complicated that you have to write yourself. In reality, DefaultConversionService already contains a solid set of built-in conversions. It can turn strings into numbers, numbers into strings, strings into boolean and back, and much more. In other words, a significant part of your parseInt(...) and Boolean.valueOf(...) calls can simply disappear from the code.

For a study project, the important thing is not to memorize the entire list of conversions (that is the road to “I learned Spring as a dictionary”), but to understand the principle: there is a standard set of converters, and it can be extended for your own types. Right now we care about this “base package” so that ConversionService is seen not as “just another utility”, but as a shared container mechanism.

A small table to make the picture more tangible:

Source value Target type What we get
"10" Integer 10
"true" Boolean true
"CSV" ReportFormat ReportFormat.CSV
"SMS" NotificationChannel NotificationChannel.SMS

The last row is especially interesting for ContextFlow: since NotificationChannel is an enum, the basic DefaultConversionService can usually already handle String -> enum. But there is a nuance: standard enum conversion is usually case-sensitive, so "SMS" is fine, but "sms" is not. And that is not a bug, just “honest default behavior”: Spring does not guess what you meant.

Let us check directly in code whether String -> NotificationChannel is supported:

import org.springframework.core.convert.ConversionService;

// Check that String -> enum conversion is registered in principle
boolean ok = conversionService.canConvert(String.class, NotificationChannel.class);
System.out.println("String -> NotificationChannel supported = " + ok); // true

If you get this true, then the first version of typed configuration is already possible: it is enough for properties to contain SMS, not sms. But this also shows the boundary of built-in behavior: people tend to write sms, while the container by default needs more.

3. ConversionService in pure Spring

The most practical part of the lecture is how to make ConversionService not just “created somewhere”, but actually become the shared infrastructure of the container. In plain Spring (without Boot), this is especially important: nobody will “set everything up nicely by default” for you, as in a starter. And that is a plus, because you understand the mechanism better; and a minus, because now you are responsible for the magic.

Spring has a convention: if there is a bean named conversionService in the context and it is of type ConversionService, the container uses it as the “main translator” when setting properties and binding values. So the name here is not cosmetic; it is a signal to the container: “this bean is infrastructure.”

Let us add the configuration. In a real project, you would most likely just add a method to an existing @Configuration class that handles infrastructure.

package com.example.contextflow.config.core;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;

@Configuration
public class ConversionConfig {

    @Bean
    public ConversionService conversionService() {
        // Important: the bean name must be conversionService,
        // so that Spring uses it for binding (for example, for @Value)
        return new DefaultConversionService();
    }
}

Notice the tricky detail: the bean name is taken from the method name. The method is called conversionService(), so the bean will also be called conversionService. If you name the method myCoolConversionService(), the bean will be created, but the container may not understand that this is the “main translator.” As a result, you get a strange effect: there is a ConversionService, but @Value keeps behaving as if it were not there, or behaves differently than expected.

Now let us show that this really works at the @Value-binding level. Let us make a settings bean that receives a typed enum:

package com.example.contextflow.infrastructure.notification;

import com.example.contextflow.domain.model.NotificationChannel;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class NotificationSettings {
    private final NotificationChannel defaultChannel;

    public NotificationSettings(
            // The value comes as a string from properties, but in the constructor we want a domain type
            @Value("${contextflow.notifications.default-channel}") NotificationChannel defaultChannel) {
        this.defaultChannel = defaultChannel;
    }
}

And now the key point: the value in contextflow.properties must be convertible. For standard enum conversion, this usually means “the same as the constant name.”

File: src/main/resources/contextflow.properties

# Important: by default enum conversion is case-sensitive
contextflow.notifications.default-channel=SMS

If everything is done correctly, Spring will turn the string "SMS" into NotificationChannel.SMS and pass it into the constructor. In other words, the business code will not even know there were strings somewhere. It works with a type — exactly as we wanted.

4. Programmatic conversion in code

When you first learn about ConversionService, there is a temptation: “Oh! Then I will use conversionService.convert(...) everywhere, and life will become perfect.” Not quite. The idea of the day is still that conversion should happen at the boundary of configuration and binding, while the business layer lives in types.

But there are scenarios where programmatic conversion is appropriate and even useful. For example, you read user input from the console (and ContextFlow is console-driven), get a string, and want to convert it to a domain type before going into business logic. That is still a “boundary,” just not the boundary of properties, but the boundary of input.

Suppose in one of the scenarios we want to let the operator choose a notification channel manually, just for demonstration. Then you can make a small adapter that converts a string into NotificationChannel through the shared mechanism, not through valueOf(...).

package com.example.contextflow.application.scenario;

import com.example.contextflow.domain.model.NotificationChannel;
import org.springframework.core.convert.ConversionService;
import org.springframework.stereotype.Component;

@Component
public class ConsoleChannelReader {
    // We use the same ConversionService as the container (shared conversion rules)
    private final ConversionService conversionService;

    public ConsoleChannelReader(ConversionService conversionService) {
        this.conversionService = conversionService;
    }

    public NotificationChannel readFrom(String raw) {
        // raw is user input (a string) that we convert to a domain enum
        return conversionService.convert(raw, NotificationChannel.class);
    }
}

This looks like “parsing,” but the key difference is this: inside this class, you do not know how the string is converted. You delegate that to a shared infrastructure layer. Today it is DefaultConversionService, tomorrow it is the same service with a custom converter added. Your code does not change. The infrastructure changes, and that is exactly the “spring-way” mindset.

And yes, the same nuance shows up here: if raw = "sms", standard enum conversion may fail. This is exactly where your own custom converter comes in: the shared mechanism already exists, and now it needs a clear rule for the domain type.

5. Conversion errors and fail-fast

A configuration error is not the kind of situation where you should be too polite. If you wrote contextflow.report.format=CVS instead of CSV, the application should not “somehow figure it out.” It should say: “Buddy, the config is broken.” And the sooner, the better.

In Spring’s container world this is called fail-fast: if a value cannot be converted to the required type when a bean is created, the context will not start. For production this is sometimes painful (the service did not start), but from an engineering point of view it is often good: the error did not get hidden, it surfaced right away.

Here is an example of a “bad” configuration:

# Error: value in lowercase (for enum by default this is often a problem)
contextflow.notifications.default-channel=sms

And the typed injection:

public NotificationSettings(
        // Spring will try to convert the string from properties into NotificationChannel
        @Value("${contextflow.notifications.default-channel}") NotificationChannel defaultChannel) {
    this.defaultChannel = defaultChannel;
}

At startup you will see an exception related to the inability to convert. Often somewhere in the cause chain there will be something like IllegalArgumentException about the enum value. At this moment it is important not to panic, but to be glad: the container caught the problem where it belongs — at the configuration boundary, not “after 15 minutes of scenario execution.”

For programmatic conversion, where you have user input, sometimes you want to handle the error more gently and show the user a hint. Then you can catch the exception and turn it into a clear message. For example, like this, in a simplified form:

import org.springframework.core.convert.ConversionFailedException;

try {
    // Try to convert user input into a domain enum
    return conversionService.convert(raw, NotificationChannel.class);
} catch (ConversionFailedException ex) {
    // It is better to give the user a clear message than a Spring stack trace
    throw new IllegalArgumentException("Channel must be EMAIL, SMS or CONSOLE: " + raw);
}

But in configuration (properties), it is better for the error to remain strict: if it is configured incorrectly, the context should not start. That keeps things disciplined and the system in a correct state.

6. Mini-practice in ContextFlow

Now we want not just to “take it on faith,” but to make sure with our own hands that ConversionService has become part of the container and really affects @Value behavior. This is a good moment for a small but telling experiment: we will add conversionService as an infrastructure bean, then make a typed setting, and then intentionally break the value in properties and see where and how the application fails.

First, add ConversionConfig (or the conversionService() method to an existing configuration class). Make sure the context really creates this bean and that it is available for DI (you can temporarily inject ConversionService into any @Component and print canConvert(String.class, Integer.class)).

Then pick one specific key that makes sense to you. For example, contextflow.report.format. Create the enum:

package com.example.contextflow.domain.model;

public enum ReportFormat {
    TEXT, CSV
}

And the settings bean:

package com.example.contextflow.application.reporting;

import com.example.contextflow.domain.model.ReportFormat;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class ReportingSettings {
    private final ReportFormat format;

    public ReportingSettings(@Value("${contextflow.report.format}") ReportFormat format) {
        this.format = format;
    }
}

Now set the value in properties so that it matches the enum constant:

contextflow.report.format=CSV

Run the application and make sure it starts. After that, make a “controlled self-inflicted mistake” (for educational purposes): change the value to csv in lowercase and try again. If your context fails at startup, congratulations — you have just seen the honest behavior of built-in enum conversion and confirmed that the configuration error does not get hidden.

At this point it is already clear where built-in conversion ends and why you need to add your own rule: csv, CSV, Csv and other human variations should all be reduced to one domain type in a predictable and centralized way.

7. Typical mistakes when working with ConversionService

Mistake #1: create DefaultConversionService “somewhere in the code” and think the container will start using it automatically.
Sometimes people do this: inside some @Component they write new DefaultConversionService() and use it manually. That works locally, but it does not affect @Value-binding at all, because the container does not know about this “translator.” If you want container-based conversion, ConversionService must be a context bean (and preferably under the correct name).

Mistake #2: register a bean, but call it something other than conversionService, in a creative way.
Creativity is wonderful, but Spring has conventions. If you name the bean myConversionService, you can inject it manually, but the container may not recognize it as the “main conversion service” and may not use it during binding. If you want both creativity and convention, use @Bean(name = "conversionService") and call the method anything you like, even bobTheConverter() — the bean name matters more to the container.

Mistake #3: start using conversionService.convert(...) in every business method, turning ConversionService into a “global parser.”
ConversionService is infrastructure. Its place is most often at the boundaries: configuration, input commands, adapters. If you drag it into every service “just in case,” you quickly get a new kind of hidden dependency: business logic starts living next to type conversion, and you mix layers again.

Mistake #4: expect DefaultConversionService to be “smart” and figure out case, spaces, and aliases on its own.
Built-in conversion is often strict. That is good: strict rules are easier to maintain. If you need more “human-friendly” behavior (for example, accepting sms, Sms, SMS ), you do that with an explicit rule — a custom converter. And that is the next step, not a reason to be upset with Spring.

Mistake #5: try to “fix” bad configuration with a quiet default right in the conversion layer.
Sometimes people write: “if the string is unknown, return CONSOLE and everything will work.” Yes, the application will start. But it will start with incorrect configuration, and later you will spend a long time figuring out why notifications suddenly went to the wrong place. For properties, fail-fast is better: an invalid value should break startup so the problem is detected immediately.

1
Task
Spring Core, level 13, lesson 1
Locked
Common ConversionService for Simple Conversion
Common ConversionService for Simple Conversion
1
Task
Spring Core, level 13, lesson 1
Locked
Built-in enum conversion via conversionService
Built-in enum conversion via conversionService
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION