Can Jackson be configured to trim leading/trailing whitespace from all string properties?
Asked Answered
S

7

63

Example JSON (note that the string has trailing spaces):

{ "aNumber": 0, "aString": "string   " }

Ideally, the deserialised instance would have an aString property with a value of "string" (i.e. without trailing spaces). This seems like something that is probably supported but I can't find it (e.g. in DeserializationConfig.Feature).

We're using Spring MVC 3.x so a Spring-based solution would also be fine.

I tried configuring Spring's WebDataBinder based on a suggestion in a forum post but it does not seem to work when using a Jackson message converter:

@InitBinder
public void initBinder( WebDataBinder binder )
{
    binder.registerCustomEditor( String.class, new StringTrimmerEditor( " \t\r\n\f", true ) );
}
Shippee answered 27/7, 2011 at 22:54 Comment(3)
Are you 100% sure the spaces aren't in the actual value? Because I have never seen Jackson do this. Or are you saying that the class you pass to Jackson has these trailing spaces intentionally, and you want to set up Jackson to remove it for you?Young
@matt: I thought it was pretty clearly stated that the data has trailing spaces from the source and he wants to configure Jackson to remove the trailing spaces on deserialization.Americano
That is correct, we have no valid reason to keep trailing (or leading) whitespace present in an incoming JSON message.Shippee
C
25

With a custom deserializer, you could do the following:

 <your bean>
 @JsonDeserialize(using=WhiteSpaceRemovalSerializer.class)
 public void setAString(String aString) {
    // body
 }

 <somewhere>
 public class WhiteSpaceRemovalDeserializer extends JsonDeserializer<String> {
     @Override
     public String deserialize(JsonParser jp, DeserializationContext ctxt) {
         // This is where you can deserialize your value the way you want.
         // Don't know if the following expression is correct, this is just an idea.
         return jp.getCurrentToken().asText().trim();
     }
 }

This solution does imply that this bean attribute will always be serialized this way, and you will have to annotate every attribute that you want to be deserialized this way.

Capp answered 8/8, 2011 at 22:18 Comment(9)
Thanks, although I think this.aString = aString.trim() is probably easier :-) Hopefully it will be a feature in a future version.Shippee
Although that is easier, the annotation ensures the trimming only happens when the bean is created by JSON deserialization :)Capp
@DCKing: Why not just register your custom deserializer globally via Module interface? I can't think of any bad consequences in a typical Spring app when Jackson is used only for RESTful web services, can you?Biz
@ArtemShafranov Note that if afterburner is used then registering a global deserializer won't work as afterburner optimizes those default deserializers and doesn't allow custom ones unless annotated with JsonDeserialize.Tague
Is that a good practice considering performance? What happens if the string field is null I mean, if I start adding checkings for null or if I want to remove also white spaces from in between words, special characters, wouldn't that slow down the system? Performance-wise, shouldn't this convertion and checking be done in the front end?Precritical
@Precritical I don't understand the motivation of your question. If basic input filtering is a performance concern in the first place, then I can't help but wonder how you write the other code in your application. No, this is a trivial operation and does not cost performance at all.Capp
@Capp I was just really considering possible options. I'm starting my project, just want to make it right to avoid rework or refactoring... My first idea was to create a helper class with a removeChars(object, regExp) method and in resource layer I would treat my input... something like that. Cause I'm using jpa entities and I'm not comfortable about decorating every string set method of them and wasn't sure about the performance, that's why I asked. I think I will just give it a go.Precritical
Couple things; asText() is giving me a "cannot find symbol" error. Everything else resolved to jackson.core, just not that method. Also, the link you originally posted for "custom deserializer" is broken.Bigoted
@AlwaysLearning You can now just do jp.getText().Belldame
M
37

Easy solution for Spring Boot users, just add that walv's SimpleModule extension to your application context:

package com.example;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class StringTrimModule extends SimpleModule {

    public StringTrimModule() {
        addDeserializer(String.class, new StdScalarDeserializer<String>(String.class) {
            @Override
            public String deserialize(JsonParser jsonParser, DeserializationContext ctx) throws IOException,
                    JsonProcessingException {
                return jsonParser.getValueAsString().trim();
            }
        });
    }
}

Another way to customize Jackson is to add beans of type com.fasterxml.jackson.databind.Module to your context. They will be registered with every bean of type ObjectMapper, providing a global mechanism for contributing custom modules when you add new features to your application.

http://docs.spring.io/spring-boot/docs/current/reference/html/howto-spring-mvc.html#howto-customize-the-jackson-objectmapper

if you are not using spring boot, you have to register the StringTrimModule yourself (you do not need to annotate it with @Component)

<bean class="org.springframework.http.converter.json.Jackson2Objec‌​tMapperFactoryBean">
    <property name="modulesToInstall" value="com.example.StringTrimModule"/>
</bean
Modernize answered 17/11, 2015 at 19:56 Comment(4)
How can we skip this trimming process for some specific fields? (for ex. a password field)Collectivism
The standard String deserializer does a lot more than just calling jsonParser.getValueAsString(). Also, you don't need to create a Module and register it using @Component, you can just write a deserializer and add @JsonComponent to it: @JsonComponent class TrimStringDeserializer extends StringDeserializer { @Override String deserialize(JsonParser jsonParser, DeserializationContext ctx) throws IOException { String text = super.deserialize(jsonParser, ctx) return text != null ? text.trim() : text } }Ethbun
If I want to add trimming string to serialize as well, how can I add it using addSerializer() ?Colatitude
For future readers, I had to manually add it as a module on my objectMapper, otherwise it wouldn't work objectMapper.registerModule(new StringTrimModule());Jaala
C
25

With a custom deserializer, you could do the following:

 <your bean>
 @JsonDeserialize(using=WhiteSpaceRemovalSerializer.class)
 public void setAString(String aString) {
    // body
 }

 <somewhere>
 public class WhiteSpaceRemovalDeserializer extends JsonDeserializer<String> {
     @Override
     public String deserialize(JsonParser jp, DeserializationContext ctxt) {
         // This is where you can deserialize your value the way you want.
         // Don't know if the following expression is correct, this is just an idea.
         return jp.getCurrentToken().asText().trim();
     }
 }

This solution does imply that this bean attribute will always be serialized this way, and you will have to annotate every attribute that you want to be deserialized this way.

Capp answered 8/8, 2011 at 22:18 Comment(9)
Thanks, although I think this.aString = aString.trim() is probably easier :-) Hopefully it will be a feature in a future version.Shippee
Although that is easier, the annotation ensures the trimming only happens when the bean is created by JSON deserialization :)Capp
@DCKing: Why not just register your custom deserializer globally via Module interface? I can't think of any bad consequences in a typical Spring app when Jackson is used only for RESTful web services, can you?Biz
@ArtemShafranov Note that if afterburner is used then registering a global deserializer won't work as afterburner optimizes those default deserializers and doesn't allow custom ones unless annotated with JsonDeserialize.Tague
Is that a good practice considering performance? What happens if the string field is null I mean, if I start adding checkings for null or if I want to remove also white spaces from in between words, special characters, wouldn't that slow down the system? Performance-wise, shouldn't this convertion and checking be done in the front end?Precritical
@Precritical I don't understand the motivation of your question. If basic input filtering is a performance concern in the first place, then I can't help but wonder how you write the other code in your application. No, this is a trivial operation and does not cost performance at all.Capp
@Capp I was just really considering possible options. I'm starting my project, just want to make it right to avoid rework or refactoring... My first idea was to create a helper class with a removeChars(object, regExp) method and in resource layer I would treat my input... something like that. Cause I'm using jpa entities and I'm not comfortable about decorating every string set method of them and wasn't sure about the performance, that's why I asked. I think I will just give it a go.Precritical
Couple things; asText() is giving me a "cannot find symbol" error. Everything else resolved to jackson.core, just not that method. Also, the link you originally posted for "custom deserializer" is broken.Bigoted
@AlwaysLearning You can now just do jp.getText().Belldame
L
22

I think it is better to extend default StringDeserializer as it already handles some specific cases (see here and here) that can be used by third party libraries. Below you can find configuration for Spring Boot. This is possible only with Jackson 2.9.0 and above as starting from 2.9.0 version StringDeserializer is not final anymore. If you have Jackson version below 2.9.0 you can still copy content of StringDeserializer to your code to handle above mentioned cases.

@JsonComponent
public class StringDeserializer extends com.fasterxml.jackson.databind.deser.std.StringDeserializer {

    @Override
    public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        String value = super.deserialize(p, ctxt);
        return value != null ? value.trim() : null;
    }
}
Leveloff answered 26/3, 2020 at 21:45 Comment(2)
BTW, StringUtils#trimWhitespace has shown slow benchmarks comparing to java.lang.String#trim. The java.lang.String#trim was ~10x faster in this case.Leveloff
Good answer, trim() can be replaced by Java 11 strip() which covers all the possible flavors of whitespace according to Character.isWhitespace().Roshelle
J
17

The problem of annotation @JsonDeserialize is that you must always remember to put it on the setter. To make it globally "once and forever" with Spring MVC, I did next steps:

pom.xml:

<dependency>
   <groupId>com.fasterxml.jackson.core</groupId>
   <artifactId>jackson-databind</artifactId>
   <version>2.3.3</version>
</dependency>

Create custom ObjectMapper:

package com.mycompany;

    import java.io.IOException;
    import org.apache.commons.lang3.StringUtils;
    import com.fasterxml.jackson.core.JsonParser;
    import com.fasterxml.jackson.core.JsonProcessingException;
    import com.fasterxml.jackson.databind.DeserializationContext;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer;
    import com.fasterxml.jackson.databind.module.SimpleModule;

    public class MyObjectMapper extends ObjectMapper {

        public MyObjectMapper() {
            registerModule(new MyModule());
        }
    }

    class MyModule extends SimpleModule {

        public MyModule() {
            addDeserializer(String.class, new StdScalarDeserializer<String>  (String.class) {
                @Override
                public String deserialize(JsonParser jp, DeserializationContext  ctxt) throws IOException,
                    JsonProcessingException {
                    return StringUtils.trim(jp.getValueAsString());
                }
            });
        }
    }

Update Spring's servlet-context.xml:

<bean id="objectMapper" class="com.mycompany.MyObjectMapper" />

    <mvc:annotation-driven>
        <mvc:message-converters>
            <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
                <property name="objectMapper" ref="objectMapper" />
            </bean>
        </mvc:message-converters>
    </mvc:annotation-driven>
Jujutsu answered 6/6, 2014 at 8:32 Comment(4)
This seems a nice global solution that was easy to add my Spring Boot application already using a custom ObjectMapper: I just defined and registered "StringTrimmerModule" along these lines. (Although I preferred plain old String.trim() for non-null values instead of Commons StringUtils.)Wilks
Note that if afterburner is used then this won't work as afterburner optimizes those default deserializers. If that's the case then I guess you're stuck with @JsonDeserialize?Tague
What is the impact of this solution on JSON attributes that are not strings - like numbers or booleansMcneal
@WandMaker, other types will be ignored because this deserialized is overwritten for String.class only.Jujutsu
A
8

For Spring Boot, we just have to create a custom deserializer as documented in the manual.

The following is my Groovy code but feel free to adapt it to work in Java.

import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import org.springframework.boot.jackson.JsonComponent

import static com.fasterxml.jackson.core.JsonToken.VALUE_STRING

@JsonComponent
class TrimmingJsonDeserializer extends JsonDeserializer<String> {

    @Override
    String deserialize(JsonParser parser, DeserializationContext context) {
        parser.hasToken(VALUE_STRING) ? parser.text?.trim() : null
    }
}
Amphora answered 13/7, 2017 at 11:23 Comment(1)
My observation is that Jackson uses this deserializer for all strings. If the request class contains strings for boolean, etc, then having the parser.hasToken(VALUE_STRING) will create bugs. If you have boolean, uuid, int, etc in your json request, you will get null values from JACKSON. My solution was to simply use parser.getText.trim() and it works fine on my machine :)Protoactinium
R
8

com.fasterxml.jackson.dataformat

pom.xml

   <dependency>
      <groupId>com.fasterxml.jackson.dataformat</groupId>
      <artifactId>jackson-dataformat-csv</artifactId>
      <version>2.5.3</version>
    </dependency>

CsvUtil.java

     CsvSchema bootstrapSchema = CsvSchema.emptySchema().withHeader().sortedBy();
     CsvMapper mapper = new CsvMapper();
     mapper.enable(CsvParser.Feature.TRIM_SPACES);
     InputStream inputStream = ResourceUtils.getURL(fileName).openStream();
     MappingIterator<T> readValues =
          mapper.readerFor(type).with(bootstrapSchema).readValues(inputStream);
Rostock answered 21/9, 2018 at 7:32 Comment(1)
It's ok if you want to trim all Strings, but using custom JsonDeserializer is more flexible.Lanctot
O
1

I propose you the following:

First, create a module to trim and put it into a class:

import java.io.IOException;

import org.springframework.stereotype.Component;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;

@Component
public class StringTrimModule extends SimpleModule {

    public StringTrimModule() {
        addDeserializer(String.class, new StdScalarDeserializer<String>(String.class) {
            @Override
            public String deserialize(JsonParser jsonParser, DeserializationContext ctx) throws IOException {
                return jsonParser.getValueAsString().trim();
            }
        });
    }
}

Then, create a class to configure jackson and add the module:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * Class used to configure Jackson
 */
@Configuration
public class JacksonConfiguration {

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new StringTrimModule());
        return mapper;
    }
}

That's it.

Otes answered 1/12, 2022 at 14:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.