ThreadSafetyWithPolymorphicSer5615Test.java
package tools.jackson.databind.misc;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CyclicBarrier;
import java.util.stream.Collectors;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.junit.jupiter.api.RepeatedTest;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.testutil.DatabindTestUtil;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Test for [databind#5615]: Race condition with {@code @JsonIgnoreProperties}
* and {@code @JsonTypeInfo(include = As.PROPERTY)} on serialization side.
*<p>
* NOTE: modified for [databind#1410].
*/
public class ThreadSafetyWithPolymorphicSer5615Test
extends DatabindTestUtil
{
@JsonIgnoreProperties(value = {"type"}, allowSetters = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type", visible = true)
@JsonSubTypes({
@JsonSubTypes.Type(value = LivingRoom.class, name = "Living"),
@JsonSubTypes.Type(value = SleepingRoom.class, name = "Sleeping")
})
interface Room {
@JsonProperty("typ")
RoomType getTyp();
}
record LivingRoom(@JsonProperty("typ") RoomType typ,
@JsonProperty("animals") List<Cat> animals)
implements Room
{
@Override
public RoomType getTyp() { return typ; }
}
record SleepingRoom(@JsonProperty("typ") RoomType typ,
@JsonProperty("animals") List<Dog> animals)
implements Room
{
@Override
public RoomType getTyp() { return typ; }
}
enum RoomType { Living, Sleeping }
@JsonIgnoreProperties(value = {"type"}, allowSetters = true)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type", visible = true)
@JsonSubTypes({
@JsonSubTypes.Type(value = Cat.class, name = "Cat"),
@JsonSubTypes.Type(value = Dog.class, name = "Dog")
})
interface Animal {
@JsonProperty("typ")
AnimalType getTyp();
}
record Cat(@JsonProperty("typ") AnimalType typ) implements Animal {
@Override
public AnimalType getTyp() { return typ; }
}
record Dog(@JsonProperty("typ") AnimalType typ) implements Animal {
@Override
public AnimalType getTyp() { return typ; }
}
enum AnimalType { Dog, Cat }
record Result(@JsonProperty("rooms") List<Room> rooms) { }
private static final String ROOMS_JSON = """
{
"rooms": [
{
"type": "Living",
"typ": "Living",
"animals": [
{ "type": "Cat", "typ": "Cat" },
{ "type": "Cat", "typ": "Cat" },
{ "type": "Cat", "typ": "Cat" },
{ "type": "Cat", "typ": "Cat" }
]
},
{
"type": "Sleeping",
"typ": "Sleeping",
"animals": [
{ "type": "Dog", "typ": "Dog" },
{ "type": "Dog", "typ": "Dog" },
{ "type": "Dog", "typ": "Dog" },
{ "type": "Dog", "typ": "Dog" }
]
}
]
}
""";
// Use 50 repetitions; original test uses 1000 but 50 should be enough
// to trigger the race condition
@RepeatedTest(50)
public void testConcurrentDeserializationWithJsonIgnoreAndTypeInfo() throws Exception {
final JsonMapper mapper = newJsonMapper();
final Result result = mapper.readValue(ROOMS_JSON, Result.class);
int threadCount = 10;
CyclicBarrier barrier = new CyclicBarrier(threadCount);
CopyOnWriteArrayList<Throwable> errors = new CopyOnWriteArrayList<>();
List<Thread> threads = new java.util.ArrayList<>();
for (int i = 0; i < threadCount; i++) {
Thread t = new Thread(() -> {
try {
barrier.await();
for (int j = 0; j < 100; j++) {
String json = mapper.writeValueAsString(result);
try {
mapper.readValue(json, Result.class);
} catch (Throwable e) {
// Capture the JSON that caused the failure
errors.add(new RuntimeException("Failed on JSON: " + json, e));
return;
}
}
} catch (Throwable e) {
errors.add(e);
}
});
threads.add(t);
t.start();
}
for (Thread t : threads) {
t.join();
}
assertTrue(errors.isEmpty(),
() -> String.format("test failed with %d error(s):\n%s",
errors.size(),
errors.stream()
.map(Throwable::toString)
.collect(Collectors.joining("\n"))));
}
}