EnumSerializer.java

package tools.jackson.databind.ser.jdk;

import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Set;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonFormat.Shape;

import tools.jackson.core.*;
import tools.jackson.databind.*;
import tools.jackson.databind.annotation.JacksonStdImpl;
import tools.jackson.databind.cfg.EnumFeature;
import tools.jackson.databind.cfg.MapperConfig;
import tools.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper;
import tools.jackson.databind.jsonFormatVisitors.JsonStringFormatVisitor;
import tools.jackson.databind.ser.std.StdScalarSerializer;
import tools.jackson.databind.util.EnumDefinition;
import tools.jackson.databind.util.EnumValuesToWrite;

/**
 * Standard serializer used for {@link java.lang.Enum} types.
 *<p>
 * Based on {@link StdScalarSerializer} since the JSON value is
 * scalar (String).
 */
@JacksonStdImpl
public class EnumSerializer
    extends StdScalarSerializer<Enum<?>>
{
    /**
     * Container for dynamically resolved serializations for the type.
     */
    protected final EnumValuesToWrite _enumValuesToWrite;

    /**
     * If statically known, whether to serialize as numeric index ({@code TRUE})
     * or as textual name ({@code FALSE}). {@code null} means not statically known:
     * falls back to global {@code EnumFeature.WRITE_ENUMS_USING_INDEX} at runtime.
     *<p>
     * Note: when {@code TRUE} (explicit {@code Shape.NUMBER}), numeric
     * {@code @JsonProperty} values are used as indexes; non-numeric values
     * are written as-is (as Strings).
     * When {@code null} and the global feature is enabled, ordinal is always used.
     *
     * @since 3.2 (renamed from earlier {@code _serializeAsIndex})
     */
    protected final Boolean _serializeAsNumber;

    /*
    /**********************************************************************
    /* Life-cycle
    /**********************************************************************
     */

    public EnumSerializer(EnumValuesToWrite enumValuesToWrite, Boolean serializeAsNumber)
    {
        super(enumValuesToWrite.enumClass(), false);
        _enumValuesToWrite = enumValuesToWrite;
        _serializeAsNumber = serializeAsNumber;
    }

    /**
     * Factory method used by {@link tools.jackson.databind.ser.BasicSerializerFactory}
     * for constructing serializer instance of Enum types.
     */
    public static EnumSerializer construct(Class<?> enumClass, SerializationConfig config,
            BeanDescription beanDesc, JsonFormat.Value format)
    {
        // 08-Apr-2015, tatu: As per [databind#749], we cannot statically determine
        //   between name() and toString(), need to construct `EnumValues` with names,
        //   handle toString() case dynamically (for example)
        // 26-Nov-2025, tatu: Further refactoring post-[databind#5432] to deprecate
        //   `EnumValues`, replaced with `EnumValuesToWrite`
        EnumValuesToWrite writer = EnumDefinition.construct(config, beanDesc.getClassInfo())
                .valuesToWrite(config);
        return new EnumSerializer(writer,
                _isShapeWrittenUsingNumber(enumClass, format, true, null));
    }

    /**
     * To support some level of per-property configuration, we will need
     * to make things contextual. We are limited to "textual vs numeric"
     * choice here, however.
     */
    @Override
    public ValueSerializer<?> createContextual(SerializationContext ctxt,
            BeanProperty property)
    {
        JsonFormat.Value format = findFormatOverrides(ctxt,
                property, handledType());
        if (format != null) {
            Class<?> type = handledType();
            Boolean serializeAsNumber = _isShapeWrittenUsingNumber(type,
                    format, false, _serializeAsNumber);
            if (!Objects.equals(serializeAsNumber, _serializeAsNumber)) {
                return new EnumSerializer(_enumValuesToWrite, serializeAsNumber);
            }
        }
        return this;
    }

    /*
    /**********************************************************************
    /* Extended API for Jackson databind core
    /**********************************************************************
     */

    @Deprecated // @since 3.1
    public tools.jackson.databind.util.EnumValues getEnumValues() {
        // 26-Nov-2025, tatu: Unfortunate, but can't really support getting
        //    such value, so better fail flamboyantly instead of quietly 
        throw new UnsupportedOperationException();
    }

    /*
    /**********************************************************************
    /* Actual serialization
    /**********************************************************************
     */

    @Override
    public void serialize(Enum<?> en, JsonGenerator g, SerializationContext ctxt)
        throws JacksonException
    {
        if (_serializeAsNumber != null) {
            if (_serializeAsNumber) {
                // Explicit Shape.NUMBER/ARRAY: use @JsonProperty value as number if numeric,
                // ordinal if no @JsonProperty, or @JsonProperty value as String if non-numeric
                final int nr = _enumValuesToWrite.resolvedIndexFor(en);
                if (nr >= 0) {
                    g.writeNumber(nr);
                } else {
                    final SerializableString explicitName = _enumValuesToWrite.explicitNameFor(en);
                    if (explicitName != null) {
                        // Non-numeric @JsonProperty: use as-is as String
                        g.writeString(explicitName);
                    } else {
                        // No @JsonProperty: use ordinal
                        g.writeNumber(en.ordinal());
                    }
                }
                return;
            }
            // Explicit Shape.STRING/NATURAL: fall through to textual serialization
        } else if (ctxt.isEnabled(EnumFeature.WRITE_ENUMS_USING_INDEX)) {
            // No explicit shape, global feature: use ordinal
            g.writeNumber(en.ordinal());
            return;
        }
        // Textual serialization
        final MapperConfig<?> config = ctxt.getConfig();
        if (ctxt.isEnabled(EnumFeature.WRITE_ENUMS_USING_TO_STRING)) {
            g.writeString(_enumValuesToWrite.enumValueFromToString(config, en));
        } else {
            g.writeString(_enumValuesToWrite.enumValueFromName(config, en));
        }
    }

    /*
    /**********************************************************************
    /* Schema support
    /**********************************************************************
     */

    @Override
    public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint)
    {
        SerializationContext ctxt = visitor.getContext();
        if (_serializeAsNumber(ctxt)) {
            visitIntFormat(visitor, typeHint, JsonParser.NumberType.INT);
            return;
        }
        JsonStringFormatVisitor stringVisitor = visitor.expectStringFormat(typeHint);
        if (stringVisitor != null) {
            final MapperConfig<?> config = ctxt.getConfig();
            SerializableString[] values = ctxt.isEnabled(EnumFeature.WRITE_ENUMS_USING_TO_STRING)
                    ? _enumValuesToWrite.allEnumValuesFromToString(config)
                    : _enumValuesToWrite.allEnumValuesFromName(config);
            Set<String> enumStrings = new LinkedHashSet<>();
            for (SerializableString sstr : values) {
                enumStrings.add(sstr.getValue());
            }
            stringVisitor.enumTypes(enumStrings);
        }
    }

    /*
    /**********************************************************************
    /* Helper methods
    /**********************************************************************
     */

    protected final boolean _serializeAsNumber(SerializationContext ctxt)
    {
        if (_serializeAsNumber != null) {
            return _serializeAsNumber;
        }
        return ctxt.isEnabled(EnumFeature.WRITE_ENUMS_USING_INDEX);
    }

    /**
     * Helper method called to check whether serialization should be done using
     * numeric representation or not.
     */
    protected static Boolean _isShapeWrittenUsingNumber(Class<?> enumClass,
            JsonFormat.Value format, boolean fromClass,
            Boolean defaultValue)
    {
        JsonFormat.Shape shape = (format == null) ? null : format.getShape();
        if (shape == null) {
            return defaultValue;
        }
        // i.e. "default", check dynamically
        if (shape == Shape.ANY || shape == Shape.SCALAR) {
            return defaultValue;
        }
        // 19-May-2016, tatu: also consider "natural" shape
        if (shape == Shape.STRING || shape == Shape.NATURAL) {
            return Boolean.FALSE;
        }
        // 01-Oct-2014, tatu: For convenience, consider "as-array" to also mean 'yes, use number')
        if (shape.isNumeric() || (shape == Shape.ARRAY)) {
            return Boolean.TRUE;
        }
        // 07-Mar-2017, tatu: Also means `OBJECT` not available as property annotation...
        throw new IllegalArgumentException(String.format(
                "Unsupported serialization shape (%s) for Enum %s, not supported as %s annotation",
                    shape, enumClass.getName(), (fromClass? "class" : "property")));
    }
}