From 0064c6cb59456f9e1e2d3fc90b0bd0ce2a98b52e Mon Sep 17 00:00:00 2001 From: isc-auf Date: Mon, 9 Mar 2026 18:02:10 +0100 Subject: [PATCH 01/16] Fix #5745: handle new JsonApplyView annotation --- .../databind/AnnotationIntrospector.java | 16 ++++++++++++ .../databind/SerializationContext.java | 7 +++++ .../JacksonAnnotationIntrospector.java | 7 +++++ .../databind/ser/BeanPropertyWriter.java | 16 +++++++++--- .../databind/ser/SerializationContextExt.java | 15 +++++++++++ .../databind/views/ViewSerializationTest.java | 26 +++++++++++++++++++ 6 files changed, 83 insertions(+), 4 deletions(-) diff --git a/src/main/java/tools/jackson/databind/AnnotationIntrospector.java b/src/main/java/tools/jackson/databind/AnnotationIntrospector.java index 1e41c20679..8c1352c6dc 100644 --- a/src/main/java/tools/jackson/databind/AnnotationIntrospector.java +++ b/src/main/java/tools/jackson/databind/AnnotationIntrospector.java @@ -571,6 +571,22 @@ public JacksonInject.Value findInjectableValue(MapperConfig config, Annotated */ public Class[] findViews(MapperConfig config, Annotated a) { return null; } + /** + * Method for checking if annotated property (represented by a field or + * getter/setter method) has definition for view it shall apply when processing. + * If null is returned, no view definition exist and property is processed with + * the current active view if any; + * otherwise it will use that view to process the property and its subtree + * + * @param config Effective mapper configuration in use + * @param a Annotated property (represented by a method, field or ctor parameter) + * + * @return view (represented by class) that the property will be processed with; + * if null, processing will use the current view if any + */ + public Class findApplyView(MapperConfig config, Annotated a) { return null; } + + /** * Method for finding format annotations for property or class. * Return value is typically used by serializers and/or diff --git a/src/main/java/tools/jackson/databind/SerializationContext.java b/src/main/java/tools/jackson/databind/SerializationContext.java index 2747e8d6a8..e9d79f49e1 100644 --- a/src/main/java/tools/jackson/databind/SerializationContext.java +++ b/src/main/java/tools/jackson/databind/SerializationContext.java @@ -226,6 +226,13 @@ protected SerializationContext(SerializationContext src, SerializerCache seriali _knownSerializers = src._knownSerializers; } + /* + /********************************************************************** + /* Life-cycle, factory methods + /********************************************************************** + */ + public abstract SerializationContext withConfig(SerializationConfig config); + /* /********************************************************************** /* ObjectWriteContext impl, config access diff --git a/src/main/java/tools/jackson/databind/introspect/JacksonAnnotationIntrospector.java b/src/main/java/tools/jackson/databind/introspect/JacksonAnnotationIntrospector.java index a31b6564e8..23fea81bc1 100644 --- a/src/main/java/tools/jackson/databind/introspect/JacksonAnnotationIntrospector.java +++ b/src/main/java/tools/jackson/databind/introspect/JacksonAnnotationIntrospector.java @@ -547,6 +547,13 @@ public Class[] findViews(MapperConfig config, Annotated a) return (ann == null) ? null : ann.value(); } + @Override + public Class findApplyView(MapperConfig config, Annotated a) + { + JsonApplyView ann = _findAnnotation(a, JsonApplyView.class); + return (ann == null) ? null : ann.value(); + } + @Override /** * Specific implementation that will use following tie-breaker on diff --git a/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java b/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java index cac45e7400..825e1fb83f 100644 --- a/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java +++ b/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java @@ -6,6 +6,7 @@ import java.lang.reflect.Field; import java.util.HashMap; +import com.fasterxml.jackson.annotation.JsonApplyView; import com.fasterxml.jackson.annotation.JsonInclude; import tools.jackson.core.JacksonException; @@ -645,12 +646,19 @@ public void serializeAsProperty(Object bean, JsonGenerator g, SerializationConte } g.writeName(_name); if (_typeSerializer == null) { - ser.serialize(value, g, ctxt); + ser.serialize(value, g, getContext(ctxt)); } else { - ser.serializeWithType(value, g, ctxt, _typeSerializer); + ser.serializeWithType(value, g, getContext(ctxt), _typeSerializer); } } + private SerializationContext getContext(SerializationContext context) { + AnnotationIntrospector intr = context.getAnnotationIntrospector(); + Class applyView = intr.findApplyView(context.getConfig(), this.getMember()); + context = applyView != null ? context.withConfig(context.getConfig().withView(applyView != JsonApplyView.NONE.class ? applyView : null)) : context; + return context; + } + /** * Method called to indicate that serialization of a field was omitted due * to filtering, in cases where backend data format does not allow basic @@ -713,9 +721,9 @@ public void serializeAsElement(Object bean, JsonGenerator g, SerializationContex } } if (_typeSerializer == null) { - ser.serialize(value, g, ctxt); + ser.serialize(value, g, getContext(ctxt)); } else { - ser.serializeWithType(value, g, ctxt, _typeSerializer); + ser.serializeWithType(value, g, getContext(ctxt), _typeSerializer); } } diff --git a/src/main/java/tools/jackson/databind/ser/SerializationContextExt.java b/src/main/java/tools/jackson/databind/ser/SerializationContextExt.java index 8c11eb9373..c613a2ec41 100644 --- a/src/main/java/tools/jackson/databind/ser/SerializationContextExt.java +++ b/src/main/java/tools/jackson/databind/ser/SerializationContextExt.java @@ -57,6 +57,16 @@ protected SerializationContextExt(TokenStreamFactory streamFactory, super(streamFactory, config, genSettings, f, cache); } + /* + /********************************************************************** + /* Life-cycle, factory methods + /********************************************************************** + */ + @Override + public SerializationContext withConfig(SerializationConfig config) { + return (_config == config) ? this : new SerializationContextExt(this._streamFactory, config, this._generatorConfig, this._serializerFactory, this._serializerCache); + } + /* /********************************************************************** /* Abstract method impls, factory methods @@ -505,5 +515,10 @@ public Impl(TokenStreamFactory streamFactory, SerializerFactory f, SerializerCache cache) { super(streamFactory, config, genSettings, f, cache); } + + @Override + public SerializationContext withConfig(SerializationConfig config) { + return (_config == config) ? this : new SerializationContextExt.Impl(this._streamFactory, config, this._generatorConfig, this._serializerFactory, this._serializerCache); + } } } diff --git a/src/test/java/tools/jackson/databind/views/ViewSerializationTest.java b/src/test/java/tools/jackson/databind/views/ViewSerializationTest.java index ffc49c97e8..74d286c501 100644 --- a/src/test/java/tools/jackson/databind/views/ViewSerializationTest.java +++ b/src/test/java/tools/jackson/databind/views/ViewSerializationTest.java @@ -37,6 +37,14 @@ static class Bean public String getB() { return "3"; } } + static class Bean2 { + + @JsonView(ViewA.class) + @JsonApplyView(ViewB.class) + public Bean bean = new Bean(); + + } + /** * Bean with mix of explicitly annotated * properties, and implicit ones that may or may @@ -201,4 +209,22 @@ public void test868() throws IOException assertEquals("{}", mapper.writerWithView(OtherView.class).writeValueAsString(new Foo())); } + + // [JACKSON_ANNOTATION-78] + @Test + public void testJsonApplyView() { + // Then with "ViewA", just one property + Bean2 bean2 = new Bean2(); + + StringWriter sw = new StringWriter(); + MAPPER.writerWithView(ViewA.class).writeValue(sw, bean2); + + Map map = MAPPER.readValue(sw.toString(), Map.class); + assertEquals(1, map.size()); + Map beanMap = (Map) map.get("bean"); + + assertEquals(2, beanMap.size()); + assertEquals("2", beanMap.get("aa")); + assertEquals("3", beanMap.get("b")); + } } From c2a0e0ee9561d876d42a70c7d56399a336d9804a Mon Sep 17 00:00:00 2001 From: isc-auf Date: Wed, 11 Mar 2026 15:13:18 +0100 Subject: [PATCH 02/16] Fix #5745: implements findApplyView --- .../AnnotationIntrospectorPair.java | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/main/java/tools/jackson/databind/introspect/AnnotationIntrospectorPair.java b/src/main/java/tools/jackson/databind/introspect/AnnotationIntrospectorPair.java index b2cd89b8c4..e208bcae53 100644 --- a/src/main/java/tools/jackson/databind/introspect/AnnotationIntrospectorPair.java +++ b/src/main/java/tools/jackson/databind/introspect/AnnotationIntrospectorPair.java @@ -1,13 +1,6 @@ package tools.jackson.databind.introspect; -import java.lang.annotation.Annotation; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Optional; - import com.fasterxml.jackson.annotation.*; - import tools.jackson.core.Version; import tools.jackson.databind.*; import tools.jackson.databind.annotation.JsonPOJOBuilder; @@ -18,6 +11,12 @@ import tools.jackson.databind.util.ClassUtil; import tools.jackson.databind.util.NameTransformer; +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + /** * Helper class that allows using 2 introspectors such that one * introspector acts as the primary one to use; and second one @@ -367,6 +366,20 @@ public Class[] findViews(MapperConfig config, Annotated a) { return result; } + @Override + public Class findApplyView(MapperConfig config, Annotated a) + { + /* Theoretically this could be trickier, if multiple introspectors + * return non-null entries. For now, though, we'll just consider + * first one to return non-null to win. + */ + Class result = _primary.findApplyView(config, a); + if (result == null) { + result = _secondary.findApplyView(config, a); + } + return result; + } + @Override public Boolean isTypeId(MapperConfig config, AnnotatedMember member) { Boolean b = _primary.isTypeId(config, member); From 185ef8a9892b6650408074e1fd9ed1c02dd30b28 Mon Sep 17 00:00:00 2001 From: isc-auf Date: Wed, 11 Mar 2026 22:00:26 +0100 Subject: [PATCH 03/16] Fix #5745: clones SerializationContextExt with seenObjectIds and objectIdGenerators --- .../databind/ser/SerializationContextExt.java | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/main/java/tools/jackson/databind/ser/SerializationContextExt.java b/src/main/java/tools/jackson/databind/ser/SerializationContextExt.java index c613a2ec41..73e77a9ab3 100644 --- a/src/main/java/tools/jackson/databind/ser/SerializationContextExt.java +++ b/src/main/java/tools/jackson/databind/ser/SerializationContextExt.java @@ -57,14 +57,26 @@ protected SerializationContextExt(TokenStreamFactory streamFactory, super(streamFactory, config, genSettings, f, cache); } + /* + /********************************************************************** + /* Life-cycle, secondary constructors to support + /* "mutant factories", with single property changes + /********************************************************************** + */ + private SerializationContextExt(SerializationContextExt src, SerializationConfig config) { + super(src._streamFactory, config, src._generatorConfig, src._serializerFactory, src._serializerCache); + _seenObjectIds = src._seenObjectIds; + _objectIdGenerators = src._objectIdGenerators; + } + /* /********************************************************************** /* Life-cycle, factory methods /********************************************************************** */ @Override - public SerializationContext withConfig(SerializationConfig config) { - return (_config == config) ? this : new SerializationContextExt(this._streamFactory, config, this._generatorConfig, this._serializerFactory, this._serializerCache); + public SerializationContextExt withConfig(SerializationConfig config) { + return (_config == config) ? this : new SerializationContextExt(this, config); } /* @@ -516,9 +528,13 @@ public Impl(TokenStreamFactory streamFactory, super(streamFactory, config, genSettings, f, cache); } + private Impl(SerializationContextExt.Impl src, SerializationConfig config) { + super(src, config); + } + @Override - public SerializationContext withConfig(SerializationConfig config) { - return (_config == config) ? this : new SerializationContextExt.Impl(this._streamFactory, config, this._generatorConfig, this._serializerFactory, this._serializerCache); + public SerializationContextExt.Impl withConfig(SerializationConfig config) { + return (_config == config) ? this : new SerializationContextExt.Impl(this, config); } } } From bbf5476fd72d3e02257eb6e4fb5b0629e0c738fd Mon Sep 17 00:00:00 2001 From: isc-auf Date: Fri, 13 Mar 2026 11:06:30 +0100 Subject: [PATCH 04/16] Fix #5745: performance improvement (saving applyView in BeanPropertyWriter) --- .../introspect/BeanPropertyDefinition.java | 5 ++ .../introspect/POJOPropertyBuilder.java | 5 ++ .../databind/ser/BeanPropertyWriter.java | 49 ++++++++++++++----- .../jackson/databind/ser/PropertyBuilder.java | 8 +-- 4 files changed, 51 insertions(+), 16 deletions(-) diff --git a/src/main/java/tools/jackson/databind/introspect/BeanPropertyDefinition.java b/src/main/java/tools/jackson/databind/introspect/BeanPropertyDefinition.java index 63993e2d38..57852612ba 100644 --- a/src/main/java/tools/jackson/databind/introspect/BeanPropertyDefinition.java +++ b/src/main/java/tools/jackson/databind/introspect/BeanPropertyDefinition.java @@ -241,6 +241,11 @@ public AnnotatedMember getNonConstructorMutator() { */ public Class[] findViews() { return null; } + /** + * Method used to find view to use for processing the property. + */ + public Class findApplyView() { return null; } + /** * Method used to find whether property is part of a bi-directional * reference. diff --git a/src/main/java/tools/jackson/databind/introspect/POJOPropertyBuilder.java b/src/main/java/tools/jackson/databind/introspect/POJOPropertyBuilder.java index ae6e44257f..cfbee9f688 100644 --- a/src/main/java/tools/jackson/databind/introspect/POJOPropertyBuilder.java +++ b/src/main/java/tools/jackson/databind/introspect/POJOPropertyBuilder.java @@ -826,6 +826,11 @@ public Class[] findViews() { return _annotationIntrospector.findViews(_config, getPrimaryMember()); } + @Override + public Class findApplyView() { + return _annotationIntrospector.findApplyView(_config, getPrimaryMember()); + } + @Override public AnnotationIntrospector.ReferenceProperty findReferenceType() { // 30-Mar-2017, tatu: Access lazily but retain information since it needs diff --git a/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java b/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java index 4f3d006c3f..6a6682a3fd 100644 --- a/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java +++ b/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java @@ -178,6 +178,12 @@ public class BeanPropertyWriter */ protected final Class[] _includeInViews; + /** + * Alternate set of property writers used when applyView is + * available for the Bean. + */ + protected final Class _applyView; + /** * Inclusion settings for this property, pre-computed in {@code PropertyBuilder} * by merging global defaults, type defaults, and property-level annotations, @@ -215,13 +221,29 @@ public BeanPropertyWriter(BeanPropertyDefinition propDef, { this(propDef, member, contextAnnotations, declaredType, ser, typeSer, serType, suppressNulls, suppressableValue, - includeInViews, null); + includeInViews, null, null); + } + + /** + * @deprecated Since 3.2 + */ + @Deprecated // @since 3.2 + public BeanPropertyWriter(BeanPropertyDefinition propDef, + AnnotatedMember member, Annotations contextAnnotations, + JavaType declaredType, + ValueSerializer ser, TypeSerializer typeSer, JavaType serType, + boolean suppressNulls, Object suppressableValue, + Class[] includeInViews, JsonInclude.Value inclusion) + { + this(propDef, member, contextAnnotations, declaredType, + ser, typeSer, serType, suppressNulls, suppressableValue, + includeInViews, inclusion, null); } /** - * Constructor with additional inclusion parameter. + * Constructor with additional inclusion and applyView parameter. * - * @since 3.1 + * @since 3.2 */ @SuppressWarnings("unchecked") public BeanPropertyWriter(BeanPropertyDefinition propDef, @@ -229,7 +251,7 @@ public BeanPropertyWriter(BeanPropertyDefinition propDef, JavaType declaredType, ValueSerializer ser, TypeSerializer typeSer, JavaType serType, boolean suppressNulls, Object suppressableValue, - Class[] includeInViews, JsonInclude.Value inclusion) + Class[] includeInViews, JsonInclude.Value inclusion, Class applyView) { super(propDef); _member = member; @@ -261,6 +283,7 @@ public BeanPropertyWriter(BeanPropertyDefinition propDef, // this will be resolved later on, unless nulls are to be suppressed _nullSerializer = null; _includeInViews = includeInViews; + _applyView = applyView; _inclusion = (inclusion == null) ? JsonInclude.Value.empty() : inclusion; } @@ -277,6 +300,7 @@ protected BeanPropertyWriter() { _name = null; _wrapperName = null; _includeInViews = null; + _applyView = null; _declaredType = null; _serializer = null; @@ -324,6 +348,7 @@ protected BeanPropertyWriter(BeanPropertyWriter base, PropertyName name) { _suppressNulls = base._suppressNulls; _suppressableValue = base._suppressableValue; _includeInViews = base._includeInViews; + _applyView = base._applyView; _typeSerializer = base._typeSerializer; _nonTrivialBaseType = base._nonTrivialBaseType; _inclusion = base._inclusion; @@ -348,6 +373,7 @@ protected BeanPropertyWriter(BeanPropertyWriter base, SerializedString name) { _suppressNulls = base._suppressNulls; _suppressableValue = base._suppressableValue; _includeInViews = base._includeInViews; + _applyView = base._applyView; _typeSerializer = base._typeSerializer; _nonTrivialBaseType = base._nonTrivialBaseType; _inclusion = base._inclusion; @@ -660,17 +686,14 @@ public void serializeAsProperty(Object bean, JsonGenerator g, SerializationConte } g.writeName(_name); if (_typeSerializer == null) { - ser.serialize(value, g, getContext(ctxt)); + ser.serialize(value, g, getContextForAppliedView(ctxt)); } else { - ser.serializeWithType(value, g, getContext(ctxt), _typeSerializer); + ser.serializeWithType(value, g, getContextForAppliedView(ctxt), _typeSerializer); } } - private SerializationContext getContext(SerializationContext context) { - AnnotationIntrospector intr = context.getAnnotationIntrospector(); - Class applyView = intr.findApplyView(context.getConfig(), this.getMember()); - context = applyView != null ? context.withConfig(context.getConfig().withView(applyView != JsonApplyView.NONE.class ? applyView : null)) : context; - return context; + private SerializationContext getContextForAppliedView(SerializationContext context) { + return _applyView != null && _applyView != context.getActiveView() ? context.withConfig(context.getConfig().withView(_applyView != JsonApplyView.NONE.class ? _applyView : null)) : context; } /** @@ -735,9 +758,9 @@ public void serializeAsElement(Object bean, JsonGenerator g, SerializationContex } } if (_typeSerializer == null) { - ser.serialize(value, g, getContext(ctxt)); + ser.serialize(value, g, getContextForAppliedView(ctxt)); } else { - ser.serializeWithType(value, g, getContext(ctxt), _typeSerializer); + ser.serializeWithType(value, g, getContextForAppliedView(ctxt), _typeSerializer); } } diff --git a/src/main/java/tools/jackson/databind/ser/PropertyBuilder.java b/src/main/java/tools/jackson/databind/ser/PropertyBuilder.java index b6103da1f7..4e483b5868 100644 --- a/src/main/java/tools/jackson/databind/ser/PropertyBuilder.java +++ b/src/main/java/tools/jackson/databind/ser/PropertyBuilder.java @@ -246,13 +246,15 @@ protected BeanPropertyWriter buildWriter(SerializationContext ctxt, if (views == null) { views = _beanDesc.findDefaultViews(); } + Class applyView = propDef.findApplyView(); + // [databind#1649]: Pass the computed inclusion value (which includes // contextual annotations) so BeanPropertyWriter can use it directly // instead of re-computing in findPropertyInclusion() BeanPropertyWriter bpw = _constructPropertyWriter(propDef, am, _beanDesc.getClassAnnotations(), declaredType, ser, typeSer, serializationType, suppressNulls, valueToSuppress, views, - inclV); + inclV, applyView); // How about custom null serializer? Object serDef = _annotationIntrospector.findNullSerializer(_config, am); @@ -278,12 +280,12 @@ protected BeanPropertyWriter _constructPropertyWriter(BeanPropertyDefinition pro JavaType declaredType, ValueSerializer ser, TypeSerializer typeSer, JavaType serType, boolean suppressNulls, Object suppressableValue, - Class[] includeInViews, JsonInclude.Value inclusion) + Class[] includeInViews, JsonInclude.Value inclusion, Class applyView) { return new BeanPropertyWriter(propDef, member, contextAnnotations, declaredType, ser, typeSer, serType, suppressNulls, suppressableValue, includeInViews, - inclusion); + inclusion, applyView); } /* From f8c1e2a749ada8c93fcb5c7eeaa96a5e7772fe04 Mon Sep 17 00:00:00 2001 From: isc-auf Date: Wed, 15 Apr 2026 22:42:26 +0200 Subject: [PATCH 05/16] Fix #5745: change implementation to modify activeView --- .../databind/SerializationContext.java | 12 ++---- .../AnnotationIntrospectorPair.java | 13 +++--- .../databind/ser/BeanPropertyWriter.java | 40 ++++++++++++++----- .../databind/ser/SerializationContextExt.java | 31 -------------- 4 files changed, 40 insertions(+), 56 deletions(-) diff --git a/src/main/java/tools/jackson/databind/SerializationContext.java b/src/main/java/tools/jackson/databind/SerializationContext.java index e9d79f49e1..328f92d50f 100644 --- a/src/main/java/tools/jackson/databind/SerializationContext.java +++ b/src/main/java/tools/jackson/databind/SerializationContext.java @@ -106,7 +106,7 @@ public abstract class SerializationContext /** * View used for currently active serialization, if any. */ - protected final Class _activeView; + protected Class _activeView; /* /********************************************************************** @@ -226,13 +226,6 @@ protected SerializationContext(SerializationContext src, SerializerCache seriali _knownSerializers = src._knownSerializers; } - /* - /********************************************************************** - /* Life-cycle, factory methods - /********************************************************************** - */ - public abstract SerializationContext withConfig(SerializationConfig config); - /* /********************************************************************** /* ObjectWriteContext impl, config access @@ -364,6 +357,9 @@ public JavaType constructSpecializedType(JavaType baseType, Class subclass) @Override public final Class getActiveView() { return _activeView; } + public final void setActiveView(Class activeView) { _activeView = activeView; } + + @Override public final boolean canOverrideAccessModifiers() { return _config.canOverrideAccessModifiers(); diff --git a/src/main/java/tools/jackson/databind/introspect/AnnotationIntrospectorPair.java b/src/main/java/tools/jackson/databind/introspect/AnnotationIntrospectorPair.java index c616cbec93..fa9694c1a9 100644 --- a/src/main/java/tools/jackson/databind/introspect/AnnotationIntrospectorPair.java +++ b/src/main/java/tools/jackson/databind/introspect/AnnotationIntrospectorPair.java @@ -1,6 +1,13 @@ package tools.jackson.databind.introspect; +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + import com.fasterxml.jackson.annotation.*; + import tools.jackson.core.Version; import tools.jackson.databind.*; import tools.jackson.databind.annotation.JsonPOJOBuilder; @@ -11,12 +18,6 @@ import tools.jackson.databind.util.ClassUtil; import tools.jackson.databind.util.NameTransformer; -import java.lang.annotation.Annotation; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Optional; - /** * Helper class that allows using 2 introspectors such that one * introspector acts as the primary one to use; and second one diff --git a/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java b/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java index 7dcb3a10ab..ea9d718c48 100644 --- a/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java +++ b/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java @@ -687,15 +687,18 @@ public void serializeAsProperty(Object bean, JsonGenerator g, SerializationConte } } g.writeName(_name); - if (_typeSerializer == null) { - ser.serialize(value, g, getContextForAppliedView(ctxt)); - } else { - ser.serializeWithType(value, g, getContextForAppliedView(ctxt), _typeSerializer); - } - } - private SerializationContext getContextForAppliedView(SerializationContext context) { - return _applyView != null && _applyView != context.getActiveView() ? context.withConfig(context.getConfig().withView(_applyView != JsonApplyView.NONE.class ? _applyView : null)) : context; + Class currentActiveView = ctxt.getActiveView(); + try { + ctxt.setActiveView(getAppliedView(ctxt)); + if (_typeSerializer == null) { + ser.serialize(value, g, ctxt); + } else { + ser.serializeWithType(value, g, ctxt, _typeSerializer); + } + } finally { + ctxt.setActiveView(currentActiveView); + } } /** @@ -759,10 +762,25 @@ public void serializeAsElement(Object bean, JsonGenerator g, SerializationContex return; } } - if (_typeSerializer == null) { - ser.serialize(value, g, getContextForAppliedView(ctxt)); + + Class currentActiveView = ctxt.getActiveView(); + try { + ctxt.setActiveView(getAppliedView(ctxt)); + if (_typeSerializer == null) { + ser.serialize(value, g, ctxt); + } else { + ser.serializeWithType(value, g, ctxt, _typeSerializer); + } + } finally { + ctxt.setActiveView(currentActiveView); + } + } + + private Class getAppliedView(SerializationContext context) { + if (_applyView == null) { + return context.getActiveView(); } else { - ser.serializeWithType(value, g, getContextForAppliedView(ctxt), _typeSerializer); + return _applyView != JsonApplyView.NONE.class ? _applyView : null; } } diff --git a/src/main/java/tools/jackson/databind/ser/SerializationContextExt.java b/src/main/java/tools/jackson/databind/ser/SerializationContextExt.java index 73e77a9ab3..8c11eb9373 100644 --- a/src/main/java/tools/jackson/databind/ser/SerializationContextExt.java +++ b/src/main/java/tools/jackson/databind/ser/SerializationContextExt.java @@ -57,28 +57,6 @@ protected SerializationContextExt(TokenStreamFactory streamFactory, super(streamFactory, config, genSettings, f, cache); } - /* - /********************************************************************** - /* Life-cycle, secondary constructors to support - /* "mutant factories", with single property changes - /********************************************************************** - */ - private SerializationContextExt(SerializationContextExt src, SerializationConfig config) { - super(src._streamFactory, config, src._generatorConfig, src._serializerFactory, src._serializerCache); - _seenObjectIds = src._seenObjectIds; - _objectIdGenerators = src._objectIdGenerators; - } - - /* - /********************************************************************** - /* Life-cycle, factory methods - /********************************************************************** - */ - @Override - public SerializationContextExt withConfig(SerializationConfig config) { - return (_config == config) ? this : new SerializationContextExt(this, config); - } - /* /********************************************************************** /* Abstract method impls, factory methods @@ -527,14 +505,5 @@ public Impl(TokenStreamFactory streamFactory, SerializerFactory f, SerializerCache cache) { super(streamFactory, config, genSettings, f, cache); } - - private Impl(SerializationContextExt.Impl src, SerializationConfig config) { - super(src, config); - } - - @Override - public SerializationContextExt.Impl withConfig(SerializationConfig config) { - return (_config == config) ? this : new SerializationContextExt.Impl(this, config); - } } } From bea9481c19ca98567f17892989444257b14702c6 Mon Sep 17 00:00:00 2001 From: isc-auf Date: Sat, 18 Apr 2026 18:45:50 +0200 Subject: [PATCH 06/16] Fix #5745: performance improvement (hotpath as before) --- .../databind/ser/BeanPropertyWriter.java | 59 ++++++++++--------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java b/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java index ea9d718c48..1a6b1d86b3 100644 --- a/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java +++ b/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java @@ -179,8 +179,9 @@ public class BeanPropertyWriter protected final Class[] _includeInViews; /** - * Alternate set of property writers used when applyView is - * available for the Bean. + * View to apply for this property when applyView is available for the Bean. + * + * @since 3.2 */ protected final Class _applyView; @@ -687,17 +688,25 @@ public void serializeAsProperty(Object bean, JsonGenerator g, SerializationConte } } g.writeName(_name); - - Class currentActiveView = ctxt.getActiveView(); - try { - ctxt.setActiveView(getAppliedView(ctxt)); - if (_typeSerializer == null) { - ser.serialize(value, g, ctxt); - } else { - ser.serializeWithType(value, g, ctxt, _typeSerializer); + if (_applyView == null) { + doSerialize(value, g, ctxt, ser); + } else { + Class currentActiveView = ctxt.getActiveView(); + try { + ctxt.setActiveView(_applyView != JsonApplyView.NONE.class ? _applyView : null); + doSerialize(value, g, ctxt, ser); + } + finally { + ctxt.setActiveView(currentActiveView); } - } finally { - ctxt.setActiveView(currentActiveView); + } + } + + private void doSerialize(Object value, JsonGenerator g, SerializationContext ctxt, ValueSerializer ser) { + if (_typeSerializer == null) { + ser.serialize(value, g, ctxt); + } else { + ser.serializeWithType(value, g, ctxt, _typeSerializer); } } @@ -763,27 +772,21 @@ public void serializeAsElement(Object bean, JsonGenerator g, SerializationContex } } - Class currentActiveView = ctxt.getActiveView(); - try { - ctxt.setActiveView(getAppliedView(ctxt)); - if (_typeSerializer == null) { - ser.serialize(value, g, ctxt); - } else { - ser.serializeWithType(value, g, ctxt, _typeSerializer); - } - } finally { - ctxt.setActiveView(currentActiveView); - } - } - - private Class getAppliedView(SerializationContext context) { if (_applyView == null) { - return context.getActiveView(); + doSerialize(value, g, ctxt, ser); } else { - return _applyView != JsonApplyView.NONE.class ? _applyView : null; + Class currentActiveView = ctxt.getActiveView(); + try { + ctxt.setActiveView(_applyView != JsonApplyView.NONE.class ? _applyView : null); + doSerialize(value, g, ctxt, ser); + } + finally { + ctxt.setActiveView(currentActiveView); + } } } + /** * Method called to serialize a placeholder used in tabular output when real * value is not to be included (is filtered out), but when we need an entry From d3d3070aa34c74d66ffc6f09790323dd178d5ebe Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Wed, 22 Apr 2026 14:13:38 -0700 Subject: [PATCH 07/16] ... --- .../jackson/databind/tofix/MergeWithCreator1921Test.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/test/java/tools/jackson/databind/tofix/MergeWithCreator1921Test.java b/src/test/java/tools/jackson/databind/tofix/MergeWithCreator1921Test.java index 6e2f50b30a..81ac502dbf 100644 --- a/src/test/java/tools/jackson/databind/tofix/MergeWithCreator1921Test.java +++ b/src/test/java/tools/jackson/databind/tofix/MergeWithCreator1921Test.java @@ -18,7 +18,10 @@ // improved since combination of Creator + Setter(s)/field is legit; but use // of Creator always means that operation is not true merge. // But added test just in case future brings us a good idea of way forward. -class MergeWithCreator1921Test extends DatabindTestUtil { +// +// 22-Apr-2026, tatu: issue closed, should test be deleted? +class MergeWithCreator1921Test extends DatabindTestUtil +{ static class Account { @JsonMerge(value = OptBoolean.TRUE) private final Validity validity; From d40be8a4746090f54297f62c0ee837e946acfc37 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Wed, 22 Apr 2026 14:14:39 -0700 Subject: [PATCH 08/16] undo accidental commit --- .../jackson/databind/tofix/MergeWithCreator1921Test.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/test/java/tools/jackson/databind/tofix/MergeWithCreator1921Test.java b/src/test/java/tools/jackson/databind/tofix/MergeWithCreator1921Test.java index 81ac502dbf..6e2f50b30a 100644 --- a/src/test/java/tools/jackson/databind/tofix/MergeWithCreator1921Test.java +++ b/src/test/java/tools/jackson/databind/tofix/MergeWithCreator1921Test.java @@ -18,10 +18,7 @@ // improved since combination of Creator + Setter(s)/field is legit; but use // of Creator always means that operation is not true merge. // But added test just in case future brings us a good idea of way forward. -// -// 22-Apr-2026, tatu: issue closed, should test be deleted? -class MergeWithCreator1921Test extends DatabindTestUtil -{ +class MergeWithCreator1921Test extends DatabindTestUtil { static class Account { @JsonMerge(value = OptBoolean.TRUE) private final Validity validity; From 5ba99b2b58f8b64ce8b3d46aed035b8fa7b13fa8 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Wed, 22 Apr 2026 14:25:39 -0700 Subject: [PATCH 09/16] Refactored to use new "withActiveView()" method in `SerializationContext` --- .../databind/SerializationContext.java | 3 -- .../databind/ser/BeanPropertyWriter.java | 44 ++++++++----------- 2 files changed, 18 insertions(+), 29 deletions(-) diff --git a/src/main/java/tools/jackson/databind/SerializationContext.java b/src/main/java/tools/jackson/databind/SerializationContext.java index 5849bebce6..cb816a465b 100644 --- a/src/main/java/tools/jackson/databind/SerializationContext.java +++ b/src/main/java/tools/jackson/databind/SerializationContext.java @@ -390,9 +390,6 @@ public JavaType constructSpecializedType(JavaType baseType, Class subclass) @Override public final Class getActiveView() { return _activeView; } - public final void setActiveView(Class activeView) { _activeView = activeView; } - - @Override public final boolean canOverrideAccessModifiers() { return _config.canOverrideAccessModifiers(); diff --git a/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java b/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java index 1a6b1d86b3..134625b99a 100644 --- a/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java +++ b/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java @@ -689,24 +689,11 @@ public void serializeAsProperty(Object bean, JsonGenerator g, SerializationConte } g.writeName(_name); if (_applyView == null) { - doSerialize(value, g, ctxt, ser); + _serialize(value, g, ctxt, ser); } else { - Class currentActiveView = ctxt.getActiveView(); - try { - ctxt.setActiveView(_applyView != JsonApplyView.NONE.class ? _applyView : null); - doSerialize(value, g, ctxt, ser); - } - finally { - ctxt.setActiveView(currentActiveView); - } - } - } - - private void doSerialize(Object value, JsonGenerator g, SerializationContext ctxt, ValueSerializer ser) { - if (_typeSerializer == null) { - ser.serialize(value, g, ctxt); - } else { - ser.serializeWithType(value, g, ctxt, _typeSerializer); + ValueSerializer actualSer = ser; + ctxt.withActiveView(_applyView != JsonApplyView.NONE.class ? _applyView : null, + () -> _serialize(value, g, ctxt, actualSer)); } } @@ -773,16 +760,11 @@ public void serializeAsElement(Object bean, JsonGenerator g, SerializationContex } if (_applyView == null) { - doSerialize(value, g, ctxt, ser); + _serialize(value, g, ctxt, ser); } else { - Class currentActiveView = ctxt.getActiveView(); - try { - ctxt.setActiveView(_applyView != JsonApplyView.NONE.class ? _applyView : null); - doSerialize(value, g, ctxt, ser); - } - finally { - ctxt.setActiveView(currentActiveView); - } + ValueSerializer actualSer = ser; + ctxt.withActiveView(_applyView != JsonApplyView.NONE.class ? _applyView : null, + () -> _serialize(value, g, ctxt, actualSer)); } } @@ -805,6 +787,16 @@ public void serializeAsOmittedElement(Object bean, JsonGenerator g, } } + // @since 3.2 + private final void _serialize(Object value, JsonGenerator g, SerializationContext ctxt, + ValueSerializer ser) { + if (_typeSerializer == null) { + ser.serialize(value, g, ctxt); + } else { + ser.serializeWithType(value, g, ctxt, _typeSerializer); + } + } + /* /********************************************************************** /* PropertyWriter methods (schema generation) From 118ed99413a33830f171caa8ae59e142f19160f6 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Wed, 22 Apr 2026 14:49:02 -0700 Subject: [PATCH 10/16] Remove extra line --- src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java b/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java index 134625b99a..8358d3e8cc 100644 --- a/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java +++ b/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java @@ -768,7 +768,6 @@ public void serializeAsElement(Object bean, JsonGenerator g, SerializationContex } } - /** * Method called to serialize a placeholder used in tabular output when real * value is not to be included (is filtered out), but when we need an entry From 70274a5cfdcef1438abd3cad5ccdadf9df8b812b Mon Sep 17 00:00:00 2001 From: isc-auf Date: Wed, 29 Apr 2026 22:18:50 +0200 Subject: [PATCH 11/16] Fix #5745: adding unit tests (noneView, nesting) --- .../views/ApplyViewSerializationTest.java | 129 ++++++++++++++++++ .../databind/views/ViewSerializationTest.java | 18 --- 2 files changed, 129 insertions(+), 18 deletions(-) create mode 100644 src/test/java/tools/jackson/databind/views/ApplyViewSerializationTest.java diff --git a/src/test/java/tools/jackson/databind/views/ApplyViewSerializationTest.java b/src/test/java/tools/jackson/databind/views/ApplyViewSerializationTest.java new file mode 100644 index 0000000000..ba6f7ded92 --- /dev/null +++ b/src/test/java/tools/jackson/databind/views/ApplyViewSerializationTest.java @@ -0,0 +1,129 @@ +package tools.jackson.databind.views; + +import com.fasterxml.jackson.annotation.JsonApplyView; +import com.fasterxml.jackson.annotation.JsonView; +import org.junit.jupiter.api.Test; +import tools.jackson.databind.MapperFeature; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.testutil.DatabindTestUtil; + +import java.io.StringWriter; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +//[databind#5745] : allow overriding JsonView + +/** + * Unit tests for verifying JSON apply view functionality. + */ +public class ApplyViewSerializationTest extends DatabindTestUtil +{ + // Classes that represent views + static class ViewA { } + static class ViewAA extends ViewA { } + static class ViewB { } + static class ViewBB extends ViewB { } + + static class Bean + { + @JsonView(ViewA.class) + public String a = "1"; + + @JsonView({ViewAA.class, ViewB.class}) + public String aa = "2"; + + @JsonView(ViewB.class) + public String getB() { return "3"; } + } + + static class Bean2 { + + @JsonView(ViewA.class) + @JsonApplyView(ViewB.class) + public Bean beanWithApplyViewB = new Bean(); + + @JsonView(ViewA.class) + @JsonApplyView + public Bean beanWithApplyNoneView = new Bean(); + } + + static class Bean3 { + + @JsonView(ViewA.class) + public Bean bean = new Bean(); + + @JsonView(ViewA.class) + public Bean2 bean2 = new Bean2(); + + @JsonView(ViewA.class) + @JsonApplyView(ViewB.class) + public Bean2 bean2WithApplyViewB = new Bean2(); + } + + /* + /********************************************************** + /* Unit tests + /********************************************************** + */ + + // Ensure `MapperFeature.DEFAULT_VIEW_INCLUSION` is enabled + // (its default differs b/w Jackson 2.x and 3.x) + private final ObjectMapper MAPPER = jsonMapperBuilder() + .enable(MapperFeature.DEFAULT_VIEW_INCLUSION) + .build(); + + @Test + public void testJsonApplyView() { + // Then with "ViewA", just one property + Bean2 bean2 = new Bean2(); + + StringWriter sw = new StringWriter(); + MAPPER.writerWithView(ViewA.class).writeValue(sw, bean2); + + Map bean2Map = MAPPER.readValue(sw.toString(), Map.class); + assertEquals(2, bean2Map.size()); + + Map beanWithApplyViewBMap = (Map) bean2Map.get("beanWithApplyViewB"); + assertEquals(2, beanWithApplyViewBMap.size()); + assertFalse(beanWithApplyViewBMap.containsKey("a")); + assertEquals("2", beanWithApplyViewBMap.get("aa")); + assertEquals("3", beanWithApplyViewBMap.get("b")); + + Map beanWithApplyNoneViewMap = (Map) bean2Map.get("beanWithApplyNoneView"); + assertEquals(3, beanWithApplyNoneViewMap.size()); + assertEquals("1", beanWithApplyNoneViewMap.get("a")); + assertEquals("2", beanWithApplyNoneViewMap.get("aa")); + assertEquals("3", beanWithApplyNoneViewMap.get("b")); + } + + @Test + public void testJsonApplyViewNested() { + // Then with "ViewAA", just three properties + Bean3 bean3 = new Bean3(); + + StringWriter sw = new StringWriter(); + MAPPER.writerWithView(ViewAA.class).writeValue(sw, bean3); + + Map bean3Map = MAPPER.readValue(sw.toString(), Map.class); + assertEquals(3, bean3Map.size()); + + Map beanMap = (Map) bean3Map.get("bean"); + assertEquals(2, beanMap.size()); + assertEquals("1", beanMap.get("a")); + assertEquals("2", beanMap.get("aa")); + assertFalse(beanMap.containsKey("b")); + + Map bean2Map = (Map) bean3Map.get("bean2"); + assertEquals(2, bean2Map.size()); + Map nestedBeanWithApplyViewBMap = (Map) bean2Map.get("beanWithApplyViewB"); + assertEquals(2, nestedBeanWithApplyViewBMap.size()); + assertFalse(nestedBeanWithApplyViewBMap.containsKey("a")); + assertEquals("2", nestedBeanWithApplyViewBMap.get("aa")); + assertEquals("3", nestedBeanWithApplyViewBMap.get("b")); + + Map bean2WithApplyViewBMap = (Map) bean3Map.get("bean2WithApplyViewB"); + assertEquals(0, bean2WithApplyViewBMap.size()); + } + +} diff --git a/src/test/java/tools/jackson/databind/views/ViewSerializationTest.java b/src/test/java/tools/jackson/databind/views/ViewSerializationTest.java index 87d709bbc5..3923f31545 100644 --- a/src/test/java/tools/jackson/databind/views/ViewSerializationTest.java +++ b/src/test/java/tools/jackson/databind/views/ViewSerializationTest.java @@ -217,24 +217,6 @@ public void test868() throws IOException mapper.writerWithView(OtherView.class).writeValueAsString(new Foo())); } - // [databind#5745]: allow overriding JsonView - @Test - public void testJsonApplyView() { - // Then with "ViewA", just one property - Bean2 bean2 = new Bean2(); - - StringWriter sw = new StringWriter(); - MAPPER.writerWithView(ViewA.class).writeValue(sw, bean2); - - Map map = MAPPER.readValue(sw.toString(), Map.class); - assertEquals(1, map.size()); - Map beanMap = (Map) map.get("bean"); - - assertEquals(2, beanMap.size()); - assertEquals("2", beanMap.get("aa")); - assertEquals("3", beanMap.get("b")); - } - // [databind#5937] @Test public void testWithActiveView() throws Exception From 2fe4f0a812dd86442c1f6810003266cbd5157b86 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 4 May 2026 11:33:17 -0700 Subject: [PATCH 12/16] Fix compilation problem wrt @JsonApplyView defaulting --- .../databind/views/ApplyViewSerializationTest.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/test/java/tools/jackson/databind/views/ApplyViewSerializationTest.java b/src/test/java/tools/jackson/databind/views/ApplyViewSerializationTest.java index ba6f7ded92..0dfca8ecf1 100644 --- a/src/test/java/tools/jackson/databind/views/ApplyViewSerializationTest.java +++ b/src/test/java/tools/jackson/databind/views/ApplyViewSerializationTest.java @@ -44,7 +44,7 @@ static class Bean2 { public Bean beanWithApplyViewB = new Bean(); @JsonView(ViewA.class) - @JsonApplyView + @JsonApplyView(JsonApplyView.NONE.class) public Bean beanWithApplyNoneView = new Bean(); } @@ -73,6 +73,7 @@ static class Bean3 { .enable(MapperFeature.DEFAULT_VIEW_INCLUSION) .build(); + @SuppressWarnings("unchecked") @Test public void testJsonApplyView() { // Then with "ViewA", just one property @@ -81,7 +82,7 @@ public void testJsonApplyView() { StringWriter sw = new StringWriter(); MAPPER.writerWithView(ViewA.class).writeValue(sw, bean2); - Map bean2Map = MAPPER.readValue(sw.toString(), Map.class); + Map bean2Map = MAPPER.readValue(sw.toString(), Map.class); assertEquals(2, bean2Map.size()); Map beanWithApplyViewBMap = (Map) bean2Map.get("beanWithApplyViewB"); @@ -97,6 +98,7 @@ public void testJsonApplyView() { assertEquals("3", beanWithApplyNoneViewMap.get("b")); } + @SuppressWarnings("unchecked") @Test public void testJsonApplyViewNested() { // Then with "ViewAA", just three properties @@ -105,7 +107,7 @@ public void testJsonApplyViewNested() { StringWriter sw = new StringWriter(); MAPPER.writerWithView(ViewAA.class).writeValue(sw, bean3); - Map bean3Map = MAPPER.readValue(sw.toString(), Map.class); + Map bean3Map = MAPPER.readValue(sw.toString(), Map.class); assertEquals(3, bean3Map.size()); Map beanMap = (Map) bean3Map.get("bean"); @@ -122,8 +124,7 @@ public void testJsonApplyViewNested() { assertEquals("2", nestedBeanWithApplyViewBMap.get("aa")); assertEquals("3", nestedBeanWithApplyViewBMap.get("b")); - Map bean2WithApplyViewBMap = (Map) bean3Map.get("bean2WithApplyViewB"); + Map bean2WithApplyViewBMap = (Map) bean3Map.get("bean2WithApplyViewB"); assertEquals(0, bean2WithApplyViewBMap.size()); } - } From 184f7a723f5fb8f4c9b4e3151b3995eb0c563c78 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 4 May 2026 13:27:28 -0700 Subject: [PATCH 13/16] Minor clean up --- .../views/ApplyViewSerializationTest.java | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/test/java/tools/jackson/databind/views/ApplyViewSerializationTest.java b/src/test/java/tools/jackson/databind/views/ApplyViewSerializationTest.java index 0dfca8ecf1..9d57495015 100644 --- a/src/test/java/tools/jackson/databind/views/ApplyViewSerializationTest.java +++ b/src/test/java/tools/jackson/databind/views/ApplyViewSerializationTest.java @@ -1,15 +1,17 @@ package tools.jackson.databind.views; +import java.io.StringWriter; +import java.util.Map; + +import org.junit.jupiter.api.Test; + import com.fasterxml.jackson.annotation.JsonApplyView; import com.fasterxml.jackson.annotation.JsonView; -import org.junit.jupiter.api.Test; + import tools.jackson.databind.MapperFeature; import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.testutil.DatabindTestUtil; -import java.io.StringWriter; -import java.util.Map; - import static org.junit.jupiter.api.Assertions.*; //[databind#5745] : allow overriding JsonView @@ -23,7 +25,6 @@ public class ApplyViewSerializationTest extends DatabindTestUtil static class ViewA { } static class ViewAA extends ViewA { } static class ViewB { } - static class ViewBB extends ViewB { } static class Bean { @@ -76,7 +77,8 @@ static class Bean3 { @SuppressWarnings("unchecked") @Test public void testJsonApplyView() { - // Then with "ViewA", just one property + // With "ViewA" active: both Bean2 properties are @JsonView(ViewA), so both + // serialize, but each applies its own override view to the nested Bean. Bean2 bean2 = new Bean2(); StringWriter sw = new StringWriter(); @@ -101,7 +103,8 @@ public void testJsonApplyView() { @SuppressWarnings("unchecked") @Test public void testJsonApplyViewNested() { - // Then with "ViewAA", just three properties + // With "ViewAA" active (extends ViewA): all three Bean3 properties are + // @JsonView(ViewA), so all serialize; verify nested @JsonApplyView still applies. Bean3 bean3 = new Bean3(); StringWriter sw = new StringWriter(); From 023f6825e8189c43a5a49f52629d89ec0e100a7c Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 4 May 2026 13:31:45 -0700 Subject: [PATCH 14/16] Extend test coverage --- .../views/ApplyViewSerializationTest.java | 88 ++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/src/test/java/tools/jackson/databind/views/ApplyViewSerializationTest.java b/src/test/java/tools/jackson/databind/views/ApplyViewSerializationTest.java index 9d57495015..18e8f0abb7 100644 --- a/src/test/java/tools/jackson/databind/views/ApplyViewSerializationTest.java +++ b/src/test/java/tools/jackson/databind/views/ApplyViewSerializationTest.java @@ -62,6 +62,18 @@ static class Bean3 { public Bean2 bean2WithApplyViewB = new Bean2(); } + // Used to verify that an inner @JsonApplyView overrides an outer one: + // outer writer activates ViewB; this field's @JsonView(ViewB) lets it through, + // and its @JsonApplyView(ViewA) switches the active view to ViewA for the + // nested Bean2 — without which both Bean2 fields (each @JsonView(ViewA)) + // would be filtered out by ViewB. + static class Bean4 { + + @JsonView(ViewB.class) + @JsonApplyView(ViewA.class) + public Bean2 wrapped = new Bean2(); + } + /* /********************************************************** /* Unit tests @@ -74,6 +86,12 @@ static class Bean3 { .enable(MapperFeature.DEFAULT_VIEW_INCLUSION) .build(); + // Mapper using the 3.x default (DEFAULT_VIEW_INCLUSION disabled), used to + // confirm @JsonApplyView semantics do not depend on this feature flag. + private final ObjectMapper MAPPER_NO_DEFAULT_INCLUSION = jsonMapperBuilder() + .disable(MapperFeature.DEFAULT_VIEW_INCLUSION) + .build(); + @SuppressWarnings("unchecked") @Test public void testJsonApplyView() { @@ -127,7 +145,75 @@ public void testJsonApplyViewNested() { assertEquals("2", nestedBeanWithApplyViewBMap.get("aa")); assertEquals("3", nestedBeanWithApplyViewBMap.get("b")); - Map bean2WithApplyViewBMap = (Map) bean3Map.get("bean2WithApplyViewB"); + // NONE applyView reached via nested Bean2: all three Bean properties present + Map nestedBeanWithApplyNoneViewMap = (Map) bean2Map.get("beanWithApplyNoneView"); + assertEquals(3, nestedBeanWithApplyNoneViewMap.size()); + assertEquals("1", nestedBeanWithApplyNoneViewMap.get("a")); + assertEquals("2", nestedBeanWithApplyNoneViewMap.get("aa")); + assertEquals("3", nestedBeanWithApplyNoneViewMap.get("b")); + + Map bean2WithApplyViewBMap = (Map) bean3Map.get("bean2WithApplyViewB"); assertEquals(0, bean2WithApplyViewBMap.size()); } + + // Verifies an inner @JsonApplyView overrides an outer one: writer is ViewB, + // Bean4.wrapped switches the active view to ViewA, then Bean2's own + // @JsonApplyView re-fires for the deepest Bean. + @SuppressWarnings("unchecked") + @Test + public void testJsonApplyViewOverridesOuter() { + Bean4 bean4 = new Bean4(); + + StringWriter sw = new StringWriter(); + MAPPER.writerWithView(ViewB.class).writeValue(sw, bean4); + + Map bean4Map = MAPPER.readValue(sw.toString(), Map.class); + assertEquals(1, bean4Map.size()); + + Map wrappedMap = (Map) bean4Map.get("wrapped"); + // Both Bean2 fields are @JsonView(ViewA): they only survive because Bean4 + // applied ViewA, not the writer's ViewB. + assertEquals(2, wrappedMap.size()); + + // Inner @JsonApplyView(ViewB) wins over outer ViewA for the deepest Bean + Map deepWithApplyB = (Map) wrappedMap.get("beanWithApplyViewB"); + assertEquals(2, deepWithApplyB.size()); + assertFalse(deepWithApplyB.containsKey("a")); + assertEquals("2", deepWithApplyB.get("aa")); + assertEquals("3", deepWithApplyB.get("b")); + + // Inner @JsonApplyView(NONE) wins over outer ViewA: all three properties + Map deepWithApplyNone = (Map) wrappedMap.get("beanWithApplyNoneView"); + assertEquals(3, deepWithApplyNone.size()); + assertEquals("1", deepWithApplyNone.get("a")); + assertEquals("2", deepWithApplyNone.get("aa")); + assertEquals("3", deepWithApplyNone.get("b")); + } + + // Same scenario as testJsonApplyView but with DEFAULT_VIEW_INCLUSION disabled + // (3.x default). All test properties are explicitly @JsonView-annotated, so + // the feature must not affect outcomes; in particular NONE still yields all 3. + @SuppressWarnings("unchecked") + @Test + public void testJsonApplyViewWithoutDefaultInclusion() { + Bean2 bean2 = new Bean2(); + + StringWriter sw = new StringWriter(); + MAPPER_NO_DEFAULT_INCLUSION.writerWithView(ViewA.class).writeValue(sw, bean2); + + Map bean2Map = MAPPER_NO_DEFAULT_INCLUSION.readValue(sw.toString(), Map.class); + assertEquals(2, bean2Map.size()); + + Map beanWithApplyViewBMap = (Map) bean2Map.get("beanWithApplyViewB"); + assertEquals(2, beanWithApplyViewBMap.size()); + assertFalse(beanWithApplyViewBMap.containsKey("a")); + assertEquals("2", beanWithApplyViewBMap.get("aa")); + assertEquals("3", beanWithApplyViewBMap.get("b")); + + Map beanWithApplyNoneViewMap = (Map) bean2Map.get("beanWithApplyNoneView"); + assertEquals(3, beanWithApplyNoneViewMap.size()); + assertEquals("1", beanWithApplyNoneViewMap.get("a")); + assertEquals("2", beanWithApplyNoneViewMap.get("aa")); + assertEquals("3", beanWithApplyNoneViewMap.get("b")); + } } From fdc5a72482baadd48e1799c35cbf531f67665405 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 4 May 2026 13:39:26 -0700 Subject: [PATCH 15/16] Add release notes --- release-notes/CREDITS | 4 ++++ release-notes/VERSION | 2 ++ .../tools/jackson/databind/ser/BeanPropertyWriter.java | 8 +++++--- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/release-notes/CREDITS b/release-notes/CREDITS index 08e5800bf1..7432686844 100644 --- a/release-notes/CREDITS +++ b/release-notes/CREDITS @@ -496,6 +496,10 @@ Martin Uhlen (@MartinUhlen) absent field same as explicit `null` [3.2.0] +Frederic Aubert (@f-aubert) + * Contributed #5745: Ability to change active JsonView on submodels (with `@JsonApplyView') + [3.2.0] + David Nelson (@eatdrinksleepcode) * Reported #5814: Enum deserialization does not respect `JsonFormat.Feature.ACCEPT_CASE_INSENSITIVE_VALUES` override diff --git a/release-notes/VERSION b/release-notes/VERSION index fcd1e16fa8..5c240802e2 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -180,6 +180,8 @@ Versions: 3.x (for earlier see VERSION-2.x) #5734: `DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES` treats absent field same as explicit `null` (reported by Martin U) +#5745: Ability to change active JsonView on submodels (with `@JsonApplyView') + (contributed by Frederic A) #5821: Fix dead code and side-effect bug in `BeanPropertyWriter.toString()` (fix by @pjfanning) #5851: Regression of `JsonTypeInfo.Id.MINIMAL_CLASS` in the 3.x branch diff --git a/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java b/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java index 8358d3e8cc..99363c6582 100644 --- a/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java +++ b/src/main/java/tools/jackson/databind/ser/BeanPropertyWriter.java @@ -226,7 +226,9 @@ public BeanPropertyWriter(BeanPropertyDefinition propDef, } /** - * @deprecated Since 3.2 + * @deprecated Since 3.2 use {@link #BeanPropertyWriter(BeanPropertyDefinition, + * AnnotatedMember, Annotations, JavaType, ValueSerializer, TypeSerializer, + * JavaType, boolean, Object, Class[], JsonInclude.Value, Class)} instead. */ @Deprecated // @since 3.2 public BeanPropertyWriter(BeanPropertyDefinition propDef, @@ -787,7 +789,7 @@ public void serializeAsOmittedElement(Object bean, JsonGenerator g, } // @since 3.2 - private final void _serialize(Object value, JsonGenerator g, SerializationContext ctxt, + private void _serialize(Object value, JsonGenerator g, SerializationContext ctxt, ValueSerializer ser) { if (_typeSerializer == null) { ser.serialize(value, g, ctxt); @@ -795,7 +797,7 @@ private final void _serialize(Object value, JsonGenerator g, SerializationContex ser.serializeWithType(value, g, ctxt, _typeSerializer); } } - + /* /********************************************************************** /* PropertyWriter methods (schema generation) From 9e3834cf949dccb843d68c244392312f057ea436 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 4 May 2026 13:44:23 -0700 Subject: [PATCH 16/16] Last small fixes --- release-notes/CREDITS | 2 +- release-notes/VERSION | 2 +- .../introspect/BeanPropertyDefinition.java | 14 +++++++++++++- .../databind/views/ViewSerializationTest.java | 8 -------- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/release-notes/CREDITS b/release-notes/CREDITS index 7432686844..48666f3bf6 100644 --- a/release-notes/CREDITS +++ b/release-notes/CREDITS @@ -497,7 +497,7 @@ Martin Uhlen (@MartinUhlen) [3.2.0] Frederic Aubert (@f-aubert) - * Contributed #5745: Ability to change active JsonView on submodels (with `@JsonApplyView') + * Contributed #5745: Ability to change active JsonView on submodels (with `@JsonApplyView`) [3.2.0] David Nelson (@eatdrinksleepcode) diff --git a/release-notes/VERSION b/release-notes/VERSION index 5c240802e2..ef01414e14 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -180,7 +180,7 @@ Versions: 3.x (for earlier see VERSION-2.x) #5734: `DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES` treats absent field same as explicit `null` (reported by Martin U) -#5745: Ability to change active JsonView on submodels (with `@JsonApplyView') +#5745: Ability to change active JsonView on submodels (with `@JsonApplyView`) (contributed by Frederic A) #5821: Fix dead code and side-effect bug in `BeanPropertyWriter.toString()` (fix by @pjfanning) diff --git a/src/main/java/tools/jackson/databind/introspect/BeanPropertyDefinition.java b/src/main/java/tools/jackson/databind/introspect/BeanPropertyDefinition.java index 57852612ba..fe2f278c1f 100644 --- a/src/main/java/tools/jackson/databind/introspect/BeanPropertyDefinition.java +++ b/src/main/java/tools/jackson/databind/introspect/BeanPropertyDefinition.java @@ -242,7 +242,19 @@ public AnnotatedMember getNonConstructorMutator() { public Class[] findViews() { return null; } /** - * Method used to find view to use for processing the property. + * Method used to find an override view that should be activated when + * processing this property's value (and any nested values reached through + * it), as configured by {@code @JsonApplyView}. + *

+ * Unlike {@link #findViews()} (which lists views in which the property + * itself is included), this returns the view to make active while the + * value is being serialized. Special marker + * {@code JsonApplyView.NONE} indicates that view processing should be + * disabled (active view set to {@code null}) for the property and its subtree. + * + * @return Override view to apply, or {@code null} if no override is configured + * + * @since 3.2 */ public Class findApplyView() { return null; } diff --git a/src/test/java/tools/jackson/databind/views/ViewSerializationTest.java b/src/test/java/tools/jackson/databind/views/ViewSerializationTest.java index 3923f31545..e51a601573 100644 --- a/src/test/java/tools/jackson/databind/views/ViewSerializationTest.java +++ b/src/test/java/tools/jackson/databind/views/ViewSerializationTest.java @@ -41,14 +41,6 @@ static class Bean public String getB() { return "3"; } } - static class Bean2 { - - @JsonView(ViewA.class) - @JsonApplyView(ViewB.class) - public Bean bean = new Bean(); - - } - /** * Bean with mix of explicitly annotated * properties, and implicit ones that may or may