Docs Menu
Docs Home
/ / /
Java Sync Driver
/ /

POJO Customization

On this page

  • Overview
  • Customize a PojoCodecProvider
  • ClassModel
  • PropertyModel
  • Conventions
  • Annotations
  • BsonExtraElements Example
  • BsonRepresentation Error Example
  • Discriminators
  • Advanced Configuration
  • Abstract or Interface Types in Properties
  • POJOs without No-Argument Constructors
  • Serialization Customization

In this guide, you can learn how to define custom data conversions between BSON and POJOs in the MongoDB Java driver. In our guide on POJOs, we show how to specify a PojoCodecProvider which contains classes that provide instructions on how to convert data for one or more POJO classes and their properties.

We show how to specify your data conversion using the ClassModel and PropertyModel classes. You can also learn about more specific customization from the section on Advanced Configuration.

We also show how to use helpers such as Conventions and Annotations to specify common serialization actions.

See the section on Discriminators if you want to serialize multiple POJO classes to documents in the same collection.

If you must implement conditional serialization or use enums, generics, interface types, or abstract types, see the section on Advanced Configuration.

If you are using only the predefined behavior to convert data between BSON and POJOs, you can use the automatic setting for the PojoCodecProvider shown in the Document Data Formats: POJOs guide.

This section shows you how to specify your data conversion logic and POJO classes with a PojoCodecProvider. The PojoCodecProvider is an implementation of the CodecProvider interface that specifies the Codecs to use in data conversion. Use this implementation when performing data conversion between BSON and POJOs.

You can create a PojoCodecProvider instance using the PojoCodecProvider.builder() method. You can also chain methods to the builder to register any of the following:

  • Individual POJO classes

  • Package names that contain POJO classes

  • Instances of ClassModel that describe conversion logic for a specific POJO class

The following example shows how you can specify the POJOs in a package named "org.example.pojos" and add the PojoCodecProvider to a CodecRegistry:

import org.bson.codecs.configuration.CodecProvider;
import org.bson.codecs.configuration.CodecRegistry;
import org.bson.codecs.pojo.PojoCodecProvider;
import static org.bson.codecs.configuration.CodecRegistries.fromRegistries;
import static org.bson.codecs.configuration.CodecRegistries.fromProviders;
import static com.mongodb.MongoClientSettings.getDefaultCodecRegistry;
CodecProvider pojoCodecProvider = PojoCodecProvider.builder().register("org.example.pojos").build();
CodecRegistry pojoCodecRegistry = fromRegistries(getDefaultCodecRegistry(), fromProviders(pojoCodecProvider));
// Call withCodecRegistry(pojoCodecRegistry) on an instance of MongoClient, MongoDatabase, or MongoCollection

For more information about this class, see the PojoCodecProvider.Builder API Documentation.

A ClassModel instance stores data conversion information about a specific POJO class. It contains a list of PropertyModel instances which describe the property fields of the POJO, whether to convert fields, and optionally, Codecs to convert the fields.

A ClassModel contains the following fields:

Field Name
Description

Name

The POJO class name to associate with the ClassModel.

InstanceCreatorFactory

Contains a new instance factory that creates new instances of the POJO. By default, it requires the POJO to have an empty constructor.

PropertyModels

Contains a list of PropertyModel instances that specify how to convert data to and from BSON for a field in the POJO.

IdPropertyModelHolder

Specifies the POJO field that corresponds to the document _id field. Optional.

Discriminator Key

Specifies the name of the discriminator field. Optional.
For more information about discriminators, see the Discriminators section.

Discriminator Value

Specifies the lookup value that represents the POJO class. Optional.
For more information about discriminators, see the Discriminators section.

Discriminator Flag

Specifies whether to serialize the discriminator, off by default. Optional.

For more information about this class, see the ClassModel API Documentation.

To instantiate a ClassModel, use the ClassModel.builder() method and specify your POJO class. The builder uses reflection to create the required metadata.

ClassModel<Flower> classModel = ClassModel.builder(Flower.class).build();

A PropertyModel stores information on how to serialize and deserialize a specific field in a document.

The PropertyModel contains the following information:

Field Name
Description

Name

Specifies the property name in the model.

Read Name

Name of the property to use as the key when serializing to BSON.

Write Name

Name of the property to use as the key when deserializing from BSON.

Type data

Contains an instance of org.bson.codecs.pojo.TypeData that describes the data type for the field.

Codec

Specifies a codec to use to encode or decode the field. Optional.

Serialization checker

Determines whether to serialize a value by using the criteria specified in the checker.

Property accessor

Method used to access the value of the property from the POJO.

useDiscriminator

Specifies whether to use the discriminator.
For more information about discriminators, see the Discriminators section.

To create a PropertyModel use a PropertyModelBuilder which you can instantiate by calling the PropertyModel.builder() method.

For more information about this class, see the PropertyModel.Builder API Documentation.

The Convention interface contains configuration options that modify the behavior of a ClassModel or PropertyModel. You can specify a Convention in a call to PojoCodecProvider.Builder.conventions() or to ClassModelBuilder.conventions().

Note

The builders apply Convention instances in order which can override the behavior defined in one applied earlier.

You can access the Convention instances defined in the BSON library from the following static fields in the Conventions class:

Field Name
Description

ANNOTATION_CONVENTION

Enables the annotations defined in the org.bson.codecs.pojo.annotations package for your POJO. See the section on Annotations for more information.

CLASS_AND_PROPERTY_CONVENTION

Sets the following default values for the ClassModel and PropertyModel instances:
- Discriminator key to _t
- Discriminator value to the ClassModel simple type name
- Id field to _id for each PropertyModel.

DEFAULT_CONVENTIONS

Enables the following Conventions:
- CLASS_AND_PROPERTY_CONVENTION
- ANNOTATION_CONVENTION
- OBJECT_ID_GENERATORS

NO_CONVENTIONS

Provides an empty list.

OBJECT_ID_GENERATORS

Adds a default IdGenerator that adds a new ObjectId for each ClassModel that use ObjectId values in the id property.

SET_PRIVATE_FIELDS_CONVENTION

Enables the ClassModel to set private fields using reflection without requiring a setter method.

USE_GETTERS_FOR_SETTERS

Enables use of getter methods as setters for Collection and Map fields if no setter method exists.

You can specify Conventions using one of the following methods:

To create a custom Convention, create a class that implements the Convention interface and override the apply() method from which you can access your ClassModelBuilder instance.

You can apply annotations to the getter and setter methods of a POJO class. These annotations configure the ClassModel and PropertyModel behavior for a specific field, method, or class.

The following annotations are available from the org.bson.codecs.pojo.annotations package:

Annotation Name
Description

BsonCreator

Marks a public constructor or a public static method as the creator for new instances of the class. You must annotate all parameters in the constructor with either the BsonProperty or BsonId annotations.

BsonDiscriminator

Specifies that a class uses a discriminator. You can set a custom discriminator key and value.

BsonRepresentation

Specifies the BSON type used to store the value when different from the POJO property. See an example of BsonRepresentation Error Example on this page.

BsonId

Marks a property to serialize as the _id property.

BsonIgnore

Marks a property to ignore. You can configure whether to serialize and/or deserialize a property.

BsonProperty

Specifies a custom document field name when converting the POJO field to BSON. You can include a discriminator to serialize POJOs nested within the field.

When applying @BsonProperty to a private field, you must also add getter and setter methods for that field to serialize and customize the field name.

BsonExtraElements

Specifies the POJO field on which to deserialize all elements that are not mapped to a field. The POJO field must be one of the following types:
- Map<String, Object>

The following code snippet shows a sample POJO called Product that uses several of the preceding annotations.

import org.bson.BsonType;
import org.bson.codecs.pojo.annotations.BsonCreator;
import org.bson.codecs.pojo.annotations.BsonDiscriminator;
import org.bson.codecs.pojo.annotations.BsonId;
import org.bson.codecs.pojo.annotations.BsonIgnore;
import org.bson.codecs.pojo.annotations.BsonProperty;
import org.bson.codecs.pojo.annotations.BsonRepresentation;
@BsonDiscriminator(value="AnnotatedProduct", key="_cls")
public class Product {
@BsonProperty("modelName")
private String name;
@BsonId()
@BsonRepresentation(BsonType.OBJECT_ID)
private String serialNumber;
@BsonIgnore
private List<Product> relatedItems;
@BsonCreator
public Product(@BsonProperty("modelName") String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
// ...
}

Tip

When using annotations, remember to specify the Conventions.ANNOTATION_CONVENTION in your ClassModelBuilder or PojoCodecProvider.Builder. For example:

ClassModel<Product> classModel = ClassModel.builder(Product.class).
conventions(Arrays.asList(Conventions.ANNOTATION_CONVENTION)).build();

The annotations in the example POJO specify the following behavior:

  • Reference the POJO with the specified discriminator key and value, adding the cls field with the value of "AnnotatedProduct" to the BSON document on write operations

  • Convert between the POJO name field and value and the BSON modelName field and value in the document

  • Convert between the POJO serialNumber field and value the BSON document _id field and value in the document

  • Omit the relatedItems field and value when converting data

  • Use the Product(String name) constructor when instantiating the POJO

The @BsonExtraElements annotation allows you to specify a field to deserialize data from a MongoDB document that lacks a corresponding POJO field mapping. This is useful when your application needs to work with data in a partially-defined schema. You can use this annotation to access data from any fields that do not correspond to the fields on your POJO.

Consider a situation in which you store and retrieve data for an online store using the Product POJO from the previous example. As you offer a greater variety of products for the store, you discover that you need additional fields to describe them. Instead of mapping each additional field to the POJO, you can access them from a single field annotated with @BsonExtraElements as shown in the following code example:

public class Product {
@BsonProperty("modelName")
private String name;
@BsonId()
@BsonRepresentation(BsonType.OBJECT_ID)
private String serialNumber;
@BsonIgnore
private List<Product> relatedItems;
@BsonExtraElements
private Document additionalInfo;
// ...

Suppose someone added additional fields for dimensions and weight to the product data such that the documents contained the following information:

{
"name": "MDB0123",
"serialNumber": "62e2...",
"dimensions": "3x4x5",
"weight": "256g"
}

The preceding document retrieved using the Product POJO contains the following data:

ProductWithBsonExtraElements [
name=MDB0123,
serialNumber=62eb...,
relatedItems=null,
additionalInfo=Document{{dimensions=3x4x5, weight=256g}}
]

The @BsonRepresentation annotation allows you to store a POJO class field as a different data type in your MongoDB database. The Product POJO code example in the Annotations section of this page uses @BsonRepresentation to store String values as ObjectId values in the database documents.

However, using the @BsonRepresentation annotation to convert between data types other than String and ObjectId causes the following error message:

Codec must implement RepresentationConfigurable to support BsonRepresentation

For example, the following code adds a purchaseDate field of type Long to the Product POJO. This example attempts to use @BsonRepresentation to represent Long values as DateTime values in the database:

public class Product {
@BsonProperty("modelName")
private String name;
@BsonId()
@BsonRepresentation(BsonType.OBJECT_ID)
private String serialNumber;
@BsonRepresentation(BsonType.DATE_TIME)
private Long purchaseDate;
// ...
}

The preceding code results in an error. Instead, you can create a custom Codec to convert the purchaseDate values from type Long to DateTime:

public class LongRepresentableCodec implements Codec<Long>, RepresentationConfigurable<Long> {
private final BsonType representation;
/**
* Constructs a LongRepresentableCodec with a Int64 representation.
*/
public LongRepresentableCodec() {
representation = BsonType.INT64;
}
private LongRepresentableCodec(final BsonType representation) {
this.representation = representation;
}
@Override
public BsonType getRepresentation() {
return representation;
}
@Override
public Codec<Long> withRepresentation(final BsonType representation) {
if (representation != BsonType.INT64 && representation != BsonType.DATE_TIME) {
throw new CodecConfigurationException(representation
+ " is not a supported representation for LongRepresentableCodec");
}
return new LongRepresentableCodec(representation);
}
@Override
public void encode(final BsonWriter writer, final Long value, final EncoderContext encoderContext) {
switch (representation) {
case INT64:
writer.writeInt64(value);
break;
case DATE_TIME:
writer.writeDateTime(value);
break;
default:
throw new BsonInvalidOperationException("Cannot encode a Long to a "
+ representation);
}
}
@Override
public Long decode(final BsonReader reader, final DecoderContext decoderContext) {
switch (representation) {
case INT64:
return reader.readInt64();
case DATE_TIME:
return reader.readDateTime();
default:
throw new CodecConfigurationException("Cannot decode " + representation
+ " to a Long");
}
}
@Override
public Class<Long> getEncoderClass() {
return Long.class;
}
}

Then, add an instance of the LongRepresentableCodec to your CodecRegistry, which contains a mapping between your Codec and the Java object type to which it applies. For instructions on registering your custom Codec with the CodecRegistry, see the Codecs guide.

A discriminator is a property that identifies a specific document schema. The discriminator key identifies a document field to use to identify the schema. The discriminator value identifies the default value of the document field.

Use discriminators to instruct the CodecProvider which object class to use when deserializing to different object classes from the same collection. When serializing the POJO to a MongoDB collection, the associated codec sets the discriminator key-value field, unless otherwise specified in the POJO property data.

You can set and enable a discriminator in a POJO by performing one of the following:

  • Use the @BsonDiscriminator annotation to specify the discriminator for the POJO class

  • Call enableDiscriminator(true) on the ClassModelBuilder associated with the POJO class

See the following example POJO classes that contain @BsonDiscriminator annotations and example documents that contain the discriminator fields:

@BsonDiscriminator(value="AnonymousUser", key="_cls")
public class AnonymousUser {
// class code
}
@BsonDiscriminator(value="RegisteredUser", key="_cls")
public class RegisteredUser {
// class code
}

The following shows sample documents created from the preceding POJOs in a single MongoDB collection:

{ "_cls": "AnonymousUser", "_id": ObjectId("<Object ID>"), ... }
{ "_cls": "RegisteredUser", "_id": ObjectId("<Object ID>"), ... }

To serialize a POJO that includes abstract class or interface type properties, you must specify discriminators on the type and all its subtypes or implementations.

Suppose you defined a POJO that referenced an abstract class User in one of its fields as follows:

public class UserRecordPojo {
private User user;
// ...
}

If the User abstract class has subclasses FreeUser and SubscriberUser, you can add your POJO and abstract classes to your CodecRegistry as follows:

ClassModel<UserRecordPojo> userRecordPojo = ClassModel.builder(UserRecordPojo.class).enableDiscriminator(true).build();
ClassModel<User> userModel = ClassModel.builder(User.class).enableDiscriminator(true).build();
ClassModel<FreeUser> freeUserModel = ClassModel.builder(FreeUser.class).enableDiscriminator(true).build();
ClassModel<SubscriberUser> subscriberUserModel = ClassModel.builder(SubscriberUser.class).enableDiscriminator(true).build();
PojoCodecProvider pojoCodecProvider = PojoCodecProvider.builder().register(userRecordPojo, userModel, freeUserModel, subscriberUserModel).build();
CodecRegistry pojoCodecRegistry = fromRegistries(getDefaultCodecRegistry(), fromProviders(pojoCodecProvider));

For more information about specifying discriminators, see the section of this guide on Discriminators.

The POJO Codecs default to calling the empty, no-argument constructor. To specify a different constructor, you must perform the following in your POJO:

  • pass the ANNOTATION_CONVENTION setting to your ClassModelBuilder

  • identify the constructor using the BsonCreator annotation

For an example of setting the ANNOTATION_CONVENTION, see the ANNOTATION_CONVENTION example. For an example of the BsonCreator annotation, see the POJO with annotation code example.

By default, ClassModelBuilder attempts to serialize all the non-null properties in your POJO. If a property value is null, the default PropertySerialization implementation skips that field.

You can customize your POJO serialization behavior by performing one of the following:

  • Use the @BsonIgnore annotation for a property to always skip serialization. Make sure to enable annotations using the appropriate Conventions.

  • Create a custom class that overrides the shouldSerialize() method of the PropertySerialization interface. Specify your custom implementation to the PropertyModelBuilder which you can access from the ClassModelBuilder.

For more information about how to use the @BsonIgnore annotation in a POJO, see the section of this guide on Annotations.

The following sample code shows a custom class that implements the PropertySerialization interface to override the default conditions by which to determine whether to serialize a field:

public class CourteousAgeSerialization implements PropertySerialization<Integer> {
@Override
public boolean shouldSerialize(Integer value) {
return (value < 30);
}
}

The preceding class specifies that any integer greater than 29 is not serialized, and, therefore, not included in the MongoDB document. Suppose you applied this custom serialization behavior to the following sample POJO:

public class BirthdayInvitation {
private String name;
private Integer age;
private LocalDateTime eventDateTime;
// ...
}

You can specify the custom serialization by adding the CourteousAgeSerialization instance to the PropertyModelBuilder from the ClassModel property associated with the age field using the following code:

ClassModelBuilder<BirthdayInvitation> classModel = ClassModel.builder(BirthdayInvitation.class);
((PropertyModelBuilder<Integer>) classModel.getProperty("age"))
.propertySerialization(new CourteousAgeSerialization());
PojoCodecProvider pojoCodecProvider = PojoCodecProvider.builder().register(classModel.build()).build();
CodecRegistry pojoCodecRegistry = fromRegistries(getDefaultCodecRegistry(), fromProviders(pojoCodecProvider));

If you insert a POJO that contains a value greater than 29 in the age field, the serialized document omits it. The POJO declaration and resulting document resemble the following:

// constructor with parameters for name, age, and eventDateTime, respectively
BirthdayInvitation invitation = new BirthdayInvitation(
"Galadriel",
7582,
LocalDateTime.of(2021, Month.JANUARY, 18, 30, 0)
);

Since the age field value is greater than 29, the serialized document resembles the following:

{ "_id" : ObjectId("..."), "eventDateTime" : ..., "name" : "Galadriel" }

You can use the POJO Codec to serialize classes that contain generic properties if they meet the following criteria:

  • Contain only bounded concrete type parameters

  • If it or any of its fields are part of a class hierarchy, the top-level POJO does not contain any type parameters

The ClassModelBuilder inspects and saves concrete type parameters to work around type erasure. It cannot serialize classes that contain generic properties without concrete type parameters since the JVM removes the type parameter information.

To save type parameters, you can implement the PropertyCodecProvider interface to specify them for generic types defined in a POJO. The following code snippets show an example implementation of the PropertyCodecProvider that adds serialization compatibility to the Guava Optional class.

Suppose you wanted to serialize the following POJO with Optional fields:

public class ApplicationUser {
private Optional<Address> optionalAddress;
private Optional<Subscription> optionalSubscription;
// ...
}

You can use the following implementation of PropertyCodecProvider to retrieve your custom Codec. This implementation uses the TypeWithTypeParameters interface to access the type information.

public class OptionalPropertyCodecProvider implements PropertyCodecProvider {
@Override
@SuppressWarnings({"rawtypes", "unchecked"})
public <T> Codec<T> get(final TypeWithTypeParameters<T> type, final PropertyCodecRegistry registry) {
// Check the main type and number of generic parameters
if (Optional.class.isAssignableFrom(type.getType()) && type.getTypeParameters().size() == 1) {
// Get the codec for the concrete type of the Optional, as its declared in the POJO.
Codec<?> valueCodec = registry.get(type.getTypeParameters().get(0));
return new OptionalCodec(type.getType(), valueCodec);
} else {
return null;
}
}
private static final class OptionalCodec<T> implements Codec<Optional<T>> {
private final Class<Optional<T>> encoderClass;
private final Codec<T> codec;
private OptionalCodec(final Class<Optional<T>> encoderClass, final Codec<T> codec) {
this.encoderClass = encoderClass;
this.codec = codec;
}
@Override
public void encode(final BsonWriter writer, final Optional<T> optionalValue, final EncoderContext encoderContext) {
if (optionalValue != null && optionalValue.isPresent()) {
codec.encode(writer, optionalValue.get(), encoderContext);
} else {
writer.writeNull();
}
}
@Override
public Optional<T> decode(final BsonReader reader, final DecoderContext context) {
return Optional.of(codec.decode(reader, context));
}
@Override
public Class<Optional<T>> getEncoderClass() {
return encoderClass;
}
}
}

Register your OptionalPropertyCodecProvider in your PojoCodecProvider and the package that contains your POJO as follows:

CodecProvider pojoCodecProvider = PojoCodecProvider.builder()
.register("org.example.pojos")
.register(new OptionalPropertyCodecProvider())
.build();

For more information about the methods and classes mentioned in this section, see the following API Documentation:

For more information about generics and type parameters, see the Java language guide on Invoking and Instantiating a Generic Type.

In driver versions 4.5 and later, the PojoCodecProvider no longer includes a codec to convert enum types. Ensure that you register a codec for enum types if you need one, such as the one in the default codec registry.

See the documentation on the default codec registry For more information about how to register the codecs it includes.

Back

Records