Introducing JSimpleMapper

JSimpleMapper is a small library for mapping between Java beans. It makes use of Java 1.8 lambda functions and doesn't use any reflection.

Basic Usage

A lot of problems in software development boil down to converting or mapping data.

The Webservice receives JSON data, that needs to be mapped into an object of the application. A database sends a query result, which has to be mapped to an object in our application. A device sends data with a binary protocol, that has to be parsed and mapped to a protocol model in the application. The UI works with a UI-specific ViewModel, that needs to be mapped to the model in the application...

So JSimpleMapper is a small program to map between two Java Beans, which happens a lot in layered applications.

A Java Bean is just a convention (see the Oracle Documentation), that says:

  1. All properties are private (only getters/setters)
  2. A public, no-argument constructor
  3. Implements Serializable

The basic idea of JSimpleMapper is, that you only have to implement an AbstractMapper<TSourceEntity, TTargetEntity> for mapping between two entities in your application.

Imagine, you need to convert between a PersonViewModel and PersonDomainModel, where the property PersonViewModel.birthDate is a String and PersonDomainModel.birthDate is a LocalDate.

// Copyright (c) Philipp Wagner. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

package de.bytefish.jsimplemapper;

import de.bytefish.jsimplemapper.converters.LocalDateTimeToStringConverter;
import de.bytefish.jsimplemapper.func.ICreator;
import de.bytefish.jsimplemapper.mapping.MappingResult;
import org.junit.Assert;
import org.junit.Test;

import java.time.LocalDate;

public class PersonMapperTest {

    // A View Model, that needs to be converted to a Domain Model.
    public class PersonViewModel {

        private String firstName;
        private String lastName;
        private String birthDate;

        public String getFirstName() {
            return firstName;
        }

        public void setFirstName(String firstName) {
            this.firstName = firstName;
        }

        public String getLastName() {
            return lastName;
        }

        public void setLastName(String lastName) {
            this.lastName = lastName;
        }

        public String getBirthDate() {
            return birthDate;
        }

        public void setBirthDate(String birthDate) {
            this.birthDate = birthDate;
        }
    }

    // A Domain Model with correct types, such as LocalDate for a BirthDate.
    public class PersonDomainModel {

        private String firstName;
        private String lastName;
        private LocalDate birthDate;

        public String getFirstName() {
            return firstName;
        }

        public void setFirstName(String firstName) {
            this.firstName = firstName;
        }

        public String getLastName() {
            return lastName;
        }

        public void setLastName(String lastName) {
            this.lastName = lastName;
        }

        public LocalDate getBirthDate() {
            return birthDate;
        }

        public void setBirthDate(LocalDate birthDate) {
            this.birthDate = birthDate;
        }
    }

    // Mapper between PersonViewModel and PersonDomainModel.
    public class PersonMapper extends AbstractMapper<PersonViewModel, PersonDomainModel> {

        public PersonMapper(ICreator<PersonDomainModel> creator) {
            super(creator);

            mapProperty(PersonViewModel::getFirstName, String.class, PersonDomainModel::setFirstName, String.class);
            mapProperty(PersonViewModel::getLastName, String.class, PersonDomainModel::setLastName, String.class);
            mapProperty(PersonViewModel::getBirthDate, String.class, PersonDomainModel::setBirthDate, LocalDate.class);
        }
    }

    @Test
    public void testMapping() {
        PersonMapper mapper = new PersonMapper(() -> new PersonDomainModel());

        PersonViewModel personViewModel = new PersonViewModel();

        personViewModel.setFirstName("Philipp");
        personViewModel.setLastName("Wagner");
        personViewModel.setBirthDate("1986-05-12");

        MappingResult<PersonDomainModel> result = mapper.map(personViewModel);

        Assert.assertEquals(true, result.isValid());

        PersonDomainModel personDomainModel = result.getEntity().get();

        Assert.assertEquals("Philipp", personDomainModel.getFirstName());
        Assert.assertEquals("Wagner", personDomainModel.getLastName());
        Assert.assertEquals(LocalDate.of(1986, 5, 12), personDomainModel.getBirthDate());
    }

    @Test
    public void testMapping_Error() {
        PersonMapper mapper = new PersonMapper(() -> new PersonDomainModel());

        PersonViewModel personViewModel = new PersonViewModel();

        personViewModel.setFirstName("Philipp");
        personViewModel.setLastName("Wagner");
        personViewModel.setBirthDate("1986-05");

        MappingResult<PersonDomainModel> result = mapper.map(personViewModel);

        Assert.assertEquals(false, result.isValid());
    }
}

Advanced Usage: Nested Properties

JSimpleMapper also allows to map complex nested objects. In the following example you can see how to map a flat PersonAddress class into a rich domain model Person, which has a nested Address property. You simply implement two converters for the mapping PersonAddress -> Address and the mapping PersonAddress -> Person. The PersonMapper, then defines to use the AddressMapper for the Person.address property.

// Copyright (c) Philipp Wagner. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

package de.bytefish.jsimplemapper;

import de.bytefish.jsimplemapper.func.ICreator;
import de.bytefish.jsimplemapper.mapping.MappingResult;
import org.junit.Assert;
import org.junit.Test;

public class PersonNestedEntityTest {

    public class PersonAddress {

        private String firstName;
        private String lastName;

        private String street;
        private String city;

        public String getFirstName() {
            return firstName;
        }

        public void setFirstName(String firstName) {
            this.firstName = firstName;
        }

        public String getLastName() {
            return lastName;
        }

        public void setLastName(String lastName) {
            this.lastName = lastName;
        }

        public String getStreet() {
            return street;
        }

        public void setStreet(String street) {
            this.street = street;
        }

        public String getCity() {
            return city;
        }

        public void setCity(String city) {
            this.city = city;
        }
    }

    public class Person {
        private String firstName;
        private String lastName;
        private Address address;

        public String getFirstName() {
            return firstName;
        }

        public void setFirstName(String firstName) {
            this.firstName = firstName;
        }

        public String getLastName() {
            return lastName;
        }

        public void setLastName(String lastName) {
            this.lastName = lastName;
        }

        public Address getAddress() {
            return address;
        }

        public void setAddress(Address address) {
            this.address = address;
        }
    }

    public class Address {
        private String street;
        private String city;

        public String getStreet() {
            return street;
        }

        public void setStreet(String street) {
            this.street = street;
        }

        public String getCity() {
            return city;
        }

        public void setCity(String city) {
            this.city = city;
        }
    }

    public class AddressMapper extends AbstractMapper<PersonAddress, Address> {

        public AddressMapper(ICreator<Address> creator) {
            super(creator);

            mapProperty(PersonAddress::getStreet, String.class, Address::setStreet, String.class);
            mapProperty(PersonAddress::getCity, String.class, Address::setCity, String.class);
        }
    }

    public class PersonMapper extends AbstractMapper<PersonAddress, Person> {

        public PersonMapper(ICreator<Person> creator) {
            super(creator);

            mapProperty(PersonAddress::getFirstName, String.class, Person::setFirstName, String.class);
            mapProperty(PersonAddress::getLastName, String.class, Person::setLastName, String.class);
            mapProperty(Person::setAddress, new AddressMapper(() -> new Address()));
        }
    }

    @Test
    public void testMapping_nested() {
        PersonAddress personAddress = new PersonAddress();

        personAddress.setFirstName("Philipp");
        personAddress.setLastName("Wagner");

        personAddress.setStreet("Fake Street 123");
        personAddress.setCity("Faketown");

        PersonMapper mapper = new PersonMapper(() -> new Person());

        MappingResult<Person> result = mapper.map(personAddress);

        Assert.assertEquals(true, result.isValid());

        Person person = result.getEntity().get();

        Assert.assertEquals("Philipp", person.getFirstName());
        Assert.assertEquals("Wagner", person.getLastName());

        Address address = person.getAddress();

        Assert.assertNotNull(address);

        Assert.assertEquals("Fake Street 123", address.getStreet());
        Assert.assertEquals("Faketown", address.getCity());
    }
}

How to contribute

One of the easiest ways to contribute is to participate in discussions. You can also contribute by submitting pull requests.

General feedback and discussions?

Do you have questions or feedback on this article? Please create an issue on the GitHub issue tracker.

Something is wrong or missing?

There may be something wrong or missing in this article. If you want to help fixing it, then please make a Pull Request to this file on GitHub.