Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enum should be translated using .name(), not .toString() #2048

Closed
jasperjanuar opened this issue Jan 19, 2023 · 3 comments
Closed

Enum should be translated using .name(), not .toString() #2048

jasperjanuar opened this issue Jan 19, 2023 · 3 comments
Labels
question Further information is requested

Comments

@jasperjanuar
Copy link

jasperjanuar commented Jan 19, 2023

Versions:
spring boot 2.7.4
java 17.0.4
springdoc-openapi-ui:1.6.14

Currently Enums are "translated" using their .toString()method.

public enum CustomerFeature {
  PAYOUT_APPROVAL("payoutApproval"),
  FX("fx");

  private String name;

  CustomerFeature(String name) {
    this.name = name;
  }

  @Override
  public String toString() {
    return name;
  }
}

results in
image

Describe the solution you'd like
This current workaround is to add @JsonProperty(value = "Foo") to each Enum value, but I think using Enum.name() would be better solution just as proposed and fixed in Springfox springfox/springfox#2860

@vojkny
Copy link

vojkny commented Feb 2, 2023

See this: #1247 (comment) - I was dealing with the same issue before.

@bnasslahsen
Copy link
Contributor

@jasperjanuar,

The default swagger-core implementation relies on enum toString and we will not change it.
If you want something generic you can use your own ModelConverter

import java.util.Iterator;

import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.core.converter.AnnotatedType;
import io.swagger.v3.core.converter.ModelConverter;
import io.swagger.v3.core.converter.ModelConverterContext;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.media.StringSchema;
import org.springdoc.core.providers.ObjectMapperProvider;

import org.springframework.stereotype.Component;


@Component
public class EnumToNameConverter implements ModelConverter {

	private final ObjectMapperProvider springDocObjectMapper;

	public EnumToNameConverter(ObjectMapperProvider springDocObjectMapper) {
		this.springDocObjectMapper = springDocObjectMapper;
	}


	@Override
	public Schema<?> resolve(
			AnnotatedType type,
			ModelConverterContext context,
			Iterator<ModelConverter> chain) {

		ObjectMapper mapper = springDocObjectMapper.jsonMapper();
		JavaType javaType = mapper.constructType(type.getType());
		if (javaType != null && javaType.isEnumType()) {
			Class<Enum> enumClass = (Class<Enum>) javaType.getRawClass();
			Enum[] enumConstants = enumClass.getEnumConstants();
			StringSchema stringSchema = new StringSchema();
			for (Enum en : enumConstants) {
				String enumValue = en.name();
				stringSchema.addEnumItem(enumValue);
			}
			return stringSchema;
		}
		return chain.hasNext() ? chain.next().resolve(type, context, chain) : null;
	}
}

@bnasslahsen bnasslahsen added the question Further information is requested label Feb 5, 2023
@fzoli
Copy link

fzoli commented Aug 14, 2023

Here is my solution based on response of @bnasslahsen

In my case I had to support "string enums" with special characters ( like : and - )

For example:

Original Kotlin enum:

enum class Permission(val value: String) {
    USER_CREATE("User:create"),
    ;
    override fun toString(): String {
        return value // Used by EnumToNameConverter
    }
}

Generated TypeScript enum:

export enum Permission {
    USER_CREATE = 'User:create',
}

To support the string enum in request query params (Spring):

import org.springframework.core.convert.converter.Converter
import org.springframework.web.util.UriUtils
import java.nio.charset.Charset

class PermissionConverter : Converter<String, Permission> {
	override fun convert(source: String): Permission {
		val decoded = UriUtils.decode(source, Charset.defaultCharset())
		return Permission.fromString(decoded)
	}
}

To support the string enum in request JSON:

import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.deser.std.StdDeserializer

class PermissionDeserializer : StdDeserializer<Permission>(Permission::class.java) {

	override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Permission {
		val value = p.readValueAs(String::class.java)
		return Permission.fromString(value)
	}

}

To support the string enum in response JSON:

import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.ser.std.StdSerializer

class PermissionSerializer : StdSerializer<Permission>(Permission::class.java) {

	override fun serialize(value: Permission, gen: JsonGenerator, provider: SerializerProvider) {
		gen.writeString(value.value)
	}

}

Finally the model converter:

import com.fasterxml.jackson.databind.ObjectMapper
import io.swagger.v3.core.converter.AnnotatedType
import io.swagger.v3.core.converter.ModelConverter
import io.swagger.v3.core.converter.ModelConverterContext
import io.swagger.v3.core.jackson.ModelResolver
import io.swagger.v3.oas.models.Components
import io.swagger.v3.oas.models.media.Schema
import io.swagger.v3.oas.models.media.StringSchema
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.stereotype.Component


@Component
class EnumToNameConverter(
	private val objectMapper: ObjectMapper
) : ModelConverter {

	override fun resolve(
		type: AnnotatedType,
		context: ModelConverterContext,
		chain: Iterator<ModelConverter>
	): Schema<*>? {
		val javaType = objectMapper.constructType(type.type)
		if (javaType != null && javaType.isEnumType) {
			@Suppress("UNCHECKED_CAST")
			val enumClass: Class<Enum<*>> = javaType.rawClass as Class<Enum<*>>
			val enumConstants = enumClass.enumConstants
			val stringSchema = StringSchema()
			val enumNames = mutableListOf<String>()
			for (en in enumConstants) {
				val enumName = en.name
				val enumValue = en.toString()
				enumNames.add(enumName)
				stringSchema.addEnumItem(enumValue)
			}
			val extensions = stringSchema.extensions ?: mutableMapOf()
			stringSchema.extensions = extensions
			extensions["x-enumNames"] = enumNames
			if (ModelResolver.enumsAsRef) {
				context.defineModel(enumClass.simpleName, stringSchema, type, null)
				val refSchema = Schema<Any>()
				refSchema.`$ref` = Components.COMPONENTS_SCHEMAS_REF + enumClass.simpleName
				return refSchema
			}
			return stringSchema
		}
		if (chain.hasNext()) {
			return chain.next().resolve(type, context, chain)
		}
		return null
	}

}

The solution uses extension x-enumNames to notify the code generators which variable name is preferred:

components:
  schemas:
    Permission:
      type: string
      enum:
        - User:create
      x-enumNames:
        - USER_CREATE

Note:
I think this is the "transparent" way to handle such situations.
That is why the Enum#toString() usage in the original code is considered correct,
but without the enum names extension some code generator generates source code with syntax errors.
With this extension at least I get compilable source code.
Of course I loose platform native naming conventions, but working is better than nothing.

The generated "native" TypeScript enum would look like this:

export enum Permission {
    UserCreate = 'User:create',
}

(At the end of the day it is not a real issue. What's more, it is a feature. At least each platform uses the same "constants".)

@springdoc springdoc locked as resolved and limited conversation to collaborators Aug 14, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

4 participants