-
Notifications
You must be signed in to change notification settings - Fork 6k
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
[JAVA] Enum mapping make wrong value as null instead of throwing an exception #5950
Comments
@TehBakker so basically you want to throw an exception instead of returning null, right? I think it's reasonable and to achieve that without breaking backward-compatibility, the only way seems to be adding another option (e.g. invalidEnumThrowsException) cc @cbornet |
cc @ePaul as well |
Yes that's exactly that, an extra option would be good yeah. |
I think you should be able to configure Jackson READ_UNKNOWN_ENUM_VALUES_AS_NULL from the client without the need to add another option. |
@cbornet I thought so too, I might have messed up, but I tried setting I think it's because the @JsonCreator generated take precedant over Jackson conf no ?
|
|
I've checked and it seems all jackson-based java clients set READ_ENUMS_USING_TO_STRING and WRITE_ENUMS_USING_TO_STRING. So I don't understand why we also have |
Sure, as I said in the original post, it's tied to that ticket #1919
|
@wing328 do you know if we can remove the annotations and the |
Any news on the fix making it into the next release ? |
Seems like it's related to enum marshalling: #4059 @TehBakker what about using the okhttp-gson or retrofit2 Java client instead? You can also use customized templates with the |
@wing328 actually, I think we can keep the |
@TehBakker did you test removing the annotations ? |
I've also raised #6286 which is intended to add an option in the other direction (ie. make acceptance of enums broader, by generating string constants rather than enums). I think these two options could complement each other, so codegen users would have a choice of either
@wing328 - thoughts? |
Revising what I wrote here . I think enums should return null on unknown values as per Postel's law. I'm not sure I see the interest of returning a String : what would you do with it ? |
The reason why a String would be useful instead of an enum is for handling future unknown values. If I generate a client based on a given swagger definition with enums, and then the owner of the api changes the enum by adding more values to that enum set, my client will not gracefully handle these values. So for example, I GET an object from their api, make a few changes and then PUT it back. If they add a new value to the enum, when I GET their object I will lose the value of that enum field, then when I PUT it back I will attempt to update that value to null. I have no other options other than to quickly regenerate my client every time they add a single enum value. I can't even code around the problem. Allowing me to generate Strings instead of java enums would allow me a way to code around the problem. |
Well using an ENUM means you think the values will be fixed. Right now I end up with a null value in my code, unable to know if it was a null or a wrong value inputed. |
@TehBakker agreed. As per JSON schema spec, enum is a validation keyword and should be used as such |
With the way swagger codegen currently works I can't make that choice. I am forced to use enums if the api specifies enums.
@cbornet That is great from an API Generation stand point. It communicates valid values for this field. It may not be so great as the consumer of an api though. I may not care what the field contains. I may have to code around mistakes made by the owner of the api, but with enum validation I am unable to code around those mistakes. So right now if an api specifies "enum" then I have two options. 1. My java client will contain enums, or 2. Preprocess the swagger api file and strip out all enum lists so that codegen will generate the client the way I prefer to have it (with Strings instead of enums). It would be nice if swagger codegen would help me with option 2 instead of fighting me on it. |
I understand your point if view Kevin, however I'm on the other side, as API server and I can't benefit from validation offered by the Enum keyword and have to add manual validation to see if the enum was null or a wrong value. As someone designing my server, I'm using enum because it's not supposed to change. If I was to add other value, I would make sure to communicate to people using the API or make another version of my API. However best of both world would be to leave an option to use String instead of enum for client who, like you said, don't even care for the value. |
Sorry @TehBakker for attempting to hijack your original issue. I followed #6286 to here and posted on both. The problems seem related, we want to control how the client handles enums. Whether it be more validation or less. |
The proposal in #6286 would still use the enum keyword. If you use swagger codegen to build your server, many languages (including Java/spring, which I use) will automatically generate the validation code on the server for you, so that shouldn't be an issue. (And if validation is not generated, you shouldn't be relying on client validation for your API, or you risk exposing issues in your API to clients that dont choose to use swagger to generate their API calls). The only difference would be there would be a client generation option which would interpret enums as string fields with a list of constants, rather than strictly enforcing an enum. |
Hum ? I'm not relying on client validation. Obviously on server side if the client sent a wrong value I want to throw a validation error. |
Ah ok. I understand. I think then a good option would be:
|
I'm not suggesting UNKNOWN (although that would be better than what I've now), since that would force me to not have UNKNOWN value in my Enum (which in fact I already have in some of them). I'm hoping to have my server, or at least an option to do, behave like the default Enum validation pattern, throwing a validation exception if the value does not match a value of my Enum during the derserialization process (like javax.validation annotations) |
I tend to agree with Zalando's guidelines on the subject : http://zalando.github.io/restful-api-guidelines/#112 http://zalando.github.io/restful-api-guidelines/#125 . |
any news on this one, will it be fixed or not please ? |
I am using it on the server, and agree with @TehBakker I think it should throw a validation error. I can see the possible desire to be more relaxed when generating a client, but if we make it a config option you have that. I would say that I think the default should be to be strict, both because that's what the swagger spec says (en7ums are validation) and since this is at one level a regression introduced by #1919. At least, as I read this discussion it would have been a validation error if just left to jackson, but the introduction of I'd be happy to submit a PR if there is some general agreement on what the goal is. I think #6286 is a separate issue, although I have an opinion there too, but have commented on that thread. |
Any word on this being fixed? I've just wasted two hours figuring out why READ_UNKNOWN_ENUM_VALUES_AS_NULL isn't working and it turns out it's because of the @JsonCreator annotation in the generated code. |
I had said i'd try and fix it, but we're now evaluating moving to the OpenAPI tools for code generation, and it's already fixed there, so I'm not sure if I will or not. The fix there is fromhttps://github.com/OpenAPITools/openapi-generator/issues/625 so should be relatively easy to port I guess |
I may have to do the same; I wasn't aware of the fork and progress on this tool seems to have died since. |
Hallo, |
I'm really interested in bringing this to the life either, before the universe and the project I work on all end. I'm currently working on migrating a legacy project to use the codegen and the current behavior returning Any chances to have it fixed in the v2 soon? Would be greatly appreciated. |
For those who can't wait and those who want outrace the day the Universe ends, here is a Jackson module to override the problematic use case: public final class SwaggerCodegenQuirksModule
extends SimpleModule {
private final Predicate<? super Class<?>> fixQuirks;
private SwaggerCodegenQuirksModule(final Predicate<? super Class<?>> fixQuirks) {
this.fixQuirks = fixQuirks;
}
public static Module create(final Predicate<? super Class<?>> fixQuirks) {
return new SwaggerCodegenQuirksModule(fixQuirks);
}
@Override
public void setupModule(final SetupContext setupContext) {
setupContext.addDeserializers(DeserializerRegistry.create(fixQuirks));
}
} final class DeserializerRegistry
extends SimpleDeserializers {
private final Predicate<? super Class<?>> fixQuirks;
private DeserializerRegistry(final Predicate<? super Class<?>> fixQuirks) {
this.fixQuirks = fixQuirks;
}
static Deserializers create(final Predicate<? super Class<?>> fixQuirks) {
return new DeserializerRegistry(fixQuirks);
}
@Override
@Nullable
public JsonDeserializer<?> findEnumDeserializer(final Class<?> enumClass, final DeserializationConfig config, final BeanDescription description) {
if ( !fixQuirks.test(enumClass) ) {
return null;
}
@SuppressWarnings("unchecked")
final Class<Enum<?>> castEnumClass = (Class<Enum<?>>) enumClass;
return EnumDeserializer.create(castEnumClass);
}
} final class EnumDeserializer<E extends Enum<?>>
extends StdDeserializer<E> {
private final Map<? super String, ? extends E> enumIndex;
private EnumDeserializer(final Class<E> enumClass, final Map<? super String, ? extends E> enumIndex) {
super(enumClass);
this.enumIndex = enumIndex;
}
static <E extends Enum<?>> JsonDeserializer<E> create(final Class<E> enumClass) {
try {
final Field valueField = enumClass.getDeclaredField("value");
valueField.setAccessible(true);
final Map<String, E> enumIndex = createEnumIndex(enumClass, valueField);
return new EnumDeserializer<>(enumClass, enumIndex);
} catch ( final NoSuchFieldException ex ) {
throw new RuntimeException(ex);
}
}
@Override
@Nullable
public E deserialize(final JsonParser parser, final DeserializationContext context)
throws IOException {
final String value = parser.getValueAsString();
if ( value == null ) {
return null;
}
@Nullable
final E enumValue = enumIndex.get(value);
if ( enumValue == null ) {
throw new JsonMappingException(parser, value + " does not match any of " + enumIndex.keySet());
}
return enumValue;
}
private static <E extends Enum<?>> Map<String, E> createEnumIndex(final Class<E> enumClass, final Field valueField) {
return Stream.of(enumClass.getEnumConstants())
// can be optimized returning specialized maps
.collect(Collectors.toMap(
e -> {
try {
return (String) valueField.get(e);
} catch ( final IllegalAccessException ex ) {
throw new AssertionError(ex);
}
},
Function.identity()
));
}
} Example of use: public final class SwaggerCodegenQuirksModuleTest {
private enum NonGeneratedDayOfWeek {
MON, TUE, WED, THU, FRI, SAT, SUN
}
// moved from the generated class and cleaned up for testing purposes
@Validated
@javax.annotation.Generated(value = "io.swagger.codegen.languages.SpringCodegen", date = "2020-06-26T13:14:37.578+03:00")
public static final class Generated {
public enum DayOfWeek {
MON("@@Monday@@"), // of course, no @@ in the production code, just for testing
TUE("@@Tuesday@@"),
WED("@@Wednesday@@"),
THU("@@Thursday@@"),
FRI("@@Friday@@"),
SAT("@@Saturday@@"),
SUN("@@Sunday@@");
private String value;
DayOfWeek(String value) {
this.value = value;
}
@JsonCreator
public static DayOfWeek fromValue(String text) {
for ( DayOfWeek b : DayOfWeek.values() ) {
if ( String.valueOf(b.value).equals(text) ) {
return b;
}
}
return null;
}
}
}
private static final ObjectMapper defaultObjectMapper = new ObjectMapper();
private static final ObjectMapper quirksSupportObjectMapper = new ObjectMapper()
.registerModule(SwaggerCodegenQuirksModule.create(clazz -> {
// simple check for tests only, I prefer to check for the package
return clazz == Generated.DayOfWeek.class;
}));
private static Stream<Arguments> testEnumForEquality() {
return Stream.of(
Arguments.of(defaultObjectMapper, Generated.DayOfWeek.class, "\"@@Monday@@\"", Generated.DayOfWeek.MON),
Arguments.of(defaultObjectMapper, NonGeneratedDayOfWeek.class, "\"SUN\"", NonGeneratedDayOfWeek.SUN),
Arguments.of(quirksSupportObjectMapper, Generated.DayOfWeek.class, "\"@@Tuesday@@\"", Generated.DayOfWeek.TUE),
Arguments.of(quirksSupportObjectMapper, NonGeneratedDayOfWeek.class, "\"WED\"", NonGeneratedDayOfWeek.WED)
);
}
@ParameterizedTest
@MethodSource
public <E extends Enum<?>> void testEnumForEquality(final ObjectMapper objectMapper, final Class<E> enumClass, final String json,
final E expectedEnumValue)
throws IOException {
Assertions.assertSame(expectedEnumValue, objectMapper.readValue(json, enumClass));
}
private static Stream<Arguments> testEnumForExpectedNull() {
return Stream.of(
Arguments.of(defaultObjectMapper, Generated.DayOfWeek.class, "null"),
Arguments.of(defaultObjectMapper, NonGeneratedDayOfWeek.class, "null"),
Arguments.of(quirksSupportObjectMapper, Generated.DayOfWeek.class, "null"),
Arguments.of(quirksSupportObjectMapper, NonGeneratedDayOfWeek.class, "null")
);
}
@ParameterizedTest
@MethodSource
public <E extends Enum<?>> void testEnumForExpectedNull(final ObjectMapper objectMapper, final Class<E> enumClass, final String json)
throws IOException {
Assertions.assertNull(objectMapper.readValue(json, enumClass));
}
private static Stream<Arguments> testEnumForExpectedErrorOnIllegalValue() {
return Stream.of(
Arguments.of(defaultObjectMapper, NonGeneratedDayOfWeek.class, "\"w.h.a.t.e.v.e.r\""),
Arguments.of(quirksSupportObjectMapper, Generated.DayOfWeek.class, "\"w.h.a.t.e.v.e.r\""),
Arguments.of(quirksSupportObjectMapper, NonGeneratedDayOfWeek.class, "\"w.h.a.t.e.v.e.r\"")
);
}
@ParameterizedTest
@MethodSource
public <E extends Enum<?>> void testEnumForExpectedErrorOnIllegalValue(final ObjectMapper objectMapper, final Class<E> enumClass, final String json) {
Assertions.assertThrows(JsonMappingException.class, () -> objectMapper.readValue(json, enumClass));
}
private static Stream<Arguments> testEnumForUnexpectedNullOnIllegalValue() {
return Stream.of(
Arguments.of(defaultObjectMapper, Generated.DayOfWeek.class, "\"w.h.a.t.e.v.e.r\"")
);
}
@ParameterizedTest
@MethodSource
public <E extends Enum<?>> void testEnumForUnexpectedNullOnIllegalValue(final ObjectMapper objectMapper, final Class<E> enumClass, final String json)
throws IOException {
Assertions.assertNull(objectMapper.readValue(json, enumClass));
}
} Hope it helps. |
Description
Short : If I've a value that does not match my enum, the JsonCreator of the enum make it null, where I would want it to throw a validation exception.
This is following #1919 changes
Swagger-codegen version
2.2.3-SNAPSHOT
Swagger declaration file content or url
The generated code is
Related issues
#1919
Suggest a Fix
What I would like is to be able to say if in case of unknown value, the @JsonCreator should return null like today or throw an exception.
Since my Enum is in a list attribut, I cannot just use @NotNull (and the exception message wouldn't be correct with what is really going on anyway).
Also is there a way to tell a list attribut that it should not contains any null value ?
Such as Jackson READ_UNKNOWN_ENUM_VALUES_AS_NULL property allow to declare ?
Please let me know if I'm not clear enough.
Regards
The text was updated successfully, but these errors were encountered: