AnyGetterWriter.java
package tools.jackson.databind.ser;
import java.util.Map;
import tools.jackson.core.*;
import tools.jackson.databind.*;
import tools.jackson.databind.cfg.MapperConfig;
import tools.jackson.databind.introspect.AnnotatedMember;
import tools.jackson.databind.jsonFormatVisitors.JsonObjectFormatVisitor;
import tools.jackson.databind.node.ObjectNode;
import tools.jackson.databind.ser.jdk.JDKKeySerializers;
import tools.jackson.databind.ser.jdk.MapProperty;
import tools.jackson.databind.ser.jdk.MapSerializer;
import tools.jackson.databind.util.IgnorePropertiesUtil;
/**
* Class similar to {@link BeanPropertyWriter}, but that will be used
* for serializing {@link com.fasterxml.jackson.annotation.JsonAnyGetter} annotated
* (Map) properties
*/
public class AnyGetterWriter extends BeanPropertyWriter
{
protected final BeanProperty _property;
/**
* Method (or Field) that represents the "any getter"
*/
protected final AnnotatedMember _anyGetter;
protected ValueSerializer<Object> _anySerializer;
protected MapSerializer _mapSerializer;
/**
* For {@code ObjectNode}/{@code JsonNode}-valued any-getters only: property-level
* {@code @JsonIgnoreProperties} / {@code @JsonIncludeProperties} rules to apply to
* the emitted entries. Map-valued any-getters get the same treatment through their
* {@code MapSerializer} during contextualization; the node path has no such
* serializer, so the check is captured here instead. {@code null} when no rules apply.
*
* @since 3.3
*/
protected IgnorePropertiesUtil.Checker _inclusionChecker;
/**
* @since 2.19
*/
@SuppressWarnings("unchecked")
public AnyGetterWriter(BeanPropertyWriter parent, BeanProperty property,
AnnotatedMember accessor, ValueSerializer<?> serializer)
{
super(parent);
_anyGetter = accessor;
_property = property;
_anySerializer = (ValueSerializer<Object>) serializer;
if (serializer instanceof MapSerializer mapSer) {
_mapSerializer = mapSer;
}
}
@Override
public void fixAccess(SerializationConfig config) {
_anyGetter.fixAccess(
config.isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS));
}
// Note: NOT part of ResolvableSerializer...
@SuppressWarnings("unchecked")
public void resolve(SerializationContext ctxt)
{
// Built regardless of `_anySerializer`: a custom `@JsonSerialize` on a node-valued
// any-getter leaves it non-null, but node entries are still written directly
_inclusionChecker = _buildInclusionChecker(ctxt);
// [databind#3604]: _anySerializer may be null for ObjectNode/JsonNode any-getters
if (_anySerializer == null) {
return;
}
// 05-Sep-2013, tatu: I _think_ this can be considered a primary property...
ValueSerializer<?> ser = ctxt.handlePrimaryContextualization(_anySerializer, _property);
_anySerializer = (ValueSerializer<Object>) ser;
if (ser instanceof MapSerializer mapSer) {
_mapSerializer = mapSer;
}
}
private IgnorePropertiesUtil.Checker _buildInclusionChecker(SerializationContext ctxt)
{
final AnnotationIntrospector intr = ctxt.getAnnotationIntrospector();
final AnnotatedMember member = (_property == null) ? null : _property.getMember();
if (member == null) {
return null;
}
final MapperConfig<?> config = ctxt.getConfig();
// Mirrors annotation handling in `MapSerializer.createContextual()`, minus its merge
// with pre-set ignored/included names (none exist for any-getters); keep in sync.
// (empty "ignored" set is handled by `buildCheckerIfNeeded()`)
return IgnorePropertiesUtil.buildCheckerIfNeeded(
intr.findPropertyIgnoralByName(config, member).findIgnoredForSerialization(),
intr.findPropertyInclusionByName(config, member).getIncluded());
}
private boolean _isIgnored(String name) {
return (_inclusionChecker != null) && _inclusionChecker.shouldIgnore(name);
}
public void getAndSerialize(Object bean, JsonGenerator gen, SerializationContext ctxt)
throws Exception
{
Object value = _anyGetter.getValue(bean);
if (value == null) {
return;
}
// [databind#3604]: Support ObjectNode/JsonNode for @JsonAnyGetter
if (value instanceof JsonNode) {
_serializeObjectNodeEntries(_verifyObjectNode(value, ctxt), gen, ctxt);
return;
}
if (!(value instanceof Map<?,?>)) {
ctxt.reportBadDefinition(_property.getType(), "Value returned by 'any-getter' %s() not java.util.Map but %s".formatted(
_anyGetter.getName(), value.getClass().getName()));
}
// 23-Feb-2015, tatu: Nasty, but has to do (for now)
if (_mapSerializer != null) {
_mapSerializer.serializeWithoutTypeInfo((Map<?,?>) value, gen, ctxt);
return;
}
_anySerializer.serialize(value, gen, ctxt);
}
@Override
public void serializeAsProperty(Object bean, JsonGenerator gen, SerializationContext ctxt) throws Exception {
getAndSerialize(bean, gen, ctxt);
}
/**
* [databind#6136]: The any-getter accessor name is not a property name in the
* output -- the entries it emits are -- so the filter decides inclusion per
* entry, instead of once for the accessor.
*<p>
* Note that as a result the accessor name (e.g. {@code "anyProperties"}) is no
* longer consulted as a filter key: inclusion is decided by the emitted entry
* names only. In particular {@code filterOutAllExcept(accessorName)} no longer
* includes all of its entries, and a custom {@link PropertyFilter} that excluded
* the accessor to drop the whole map no longer does so.
*
* @since 3.3
*/
@Override
public void serializeFilteredAsProperty(Object bean, JsonGenerator gen,
SerializationContext ctxt, PropertyFilter filter)
throws Exception
{
getAndFilter(bean, gen, ctxt, filter);
}
public void getAndFilter(Object bean, JsonGenerator gen, SerializationContext ctxt,
PropertyFilter filter)
throws Exception
{
Object value = _anyGetter.getValue(bean);
if (value == null) {
return;
}
// [databind#3604]: Support ObjectNode/JsonNode for @JsonAnyGetter
if (value instanceof JsonNode) {
_serializeFilteredObjectNodeEntries(_verifyObjectNode(value, ctxt), gen, ctxt,
bean, filter);
return;
}
if (!(value instanceof Map<?,?>)) {
ctxt.reportBadDefinition(_property.getType(),
"Value returned by 'any-getter' (%s()) not java.util.Map but %s".formatted(
_anyGetter.getName(), value.getClass().getName()));
}
if (_mapSerializer != null) {
_mapSerializer.serializeFilteredAnyProperties(ctxt, gen, bean, (Map<?,?>) value,
filter);
return;
}
// ... not sure how custom handler would do it
_anySerializer.serialize(value, gen, ctxt);
}
/**
* Helper method to verify that a {@link JsonNode} value is an {@link ObjectNode},
* throwing a clear error if not (e.g. ArrayNode).
*
* @since 3.2
*/
protected ObjectNode _verifyObjectNode(Object value, SerializationContext ctxt)
throws DatabindException
{
if (value instanceof ObjectNode objectNode) {
return objectNode;
}
return ctxt.reportBadDefinition(_property.getType(), String.format(
"Value returned by 'any-getter' %s not `ObjectNode` but `%s`; only `ObjectNode`s can be used as `@JsonAnyGetter` values",
_anyGetter.getName(), value.getClass().getName()));
}
/**
* Helper method for serializing entries of an {@link ObjectNode}
* as individual properties (for {@code @JsonAnyGetter} support).
*
* @since 3.2
*/
protected void _serializeObjectNodeEntries(ObjectNode objectNode,
JsonGenerator gen, SerializationContext ctxt)
throws Exception
{
for (Map.Entry<String, JsonNode> entry : objectNode.properties()) {
if (_isIgnored(entry.getKey())) {
continue;
}
gen.writeName(entry.getKey());
entry.getValue().serialize(gen, ctxt);
}
}
/**
* Variant of {@link #_serializeObjectNodeEntries} used when a JSON Filter is in
* effect: entries become properties of the enclosing POJO, so each one is passed
* to the filter the same way {@code MapSerializer} passes entries of a
* {@link Map}-valued any-getter.
*
* @since 3.3
*/
protected void _serializeFilteredObjectNodeEntries(ObjectNode objectNode,
JsonGenerator gen, SerializationContext ctxt,
Object bean, PropertyFilter filter)
throws Exception
{
final MapProperty prop = new MapProperty(null, _property);
// plain names, same as `_serializeObjectNodeEntries()` (no custom key serializer)
final ValueSerializer<Object> keySer = JDKKeySerializers.getStdKeySerializer(
ctxt.getConfig(), String.class, false);
for (Map.Entry<String, JsonNode> entry : objectNode.properties()) {
if (_isIgnored(entry.getKey())) {
continue;
}
final JsonNode v = entry.getValue();
prop.reset(entry.getKey(), v, keySer, ctxt.findValueSerializer(v.getClass()));
filter.serializeAsProperty(bean, gen, ctxt, prop);
}
}
@Override
public void depositSchemaProperty(JsonObjectFormatVisitor v, SerializationContext ctxt)
{
// no-op
}
}