← Назад к вопросам

Как определить пользовательский тип данных в JSON?

1.0 Junior🔥 131 комментариев
#REST API и микросервисы#Основы Java

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

# Определение пользовательских типов данных в JSON

JSON сам по себе поддерживает только базовые типы: string, number, boolean, null, array, object. Для работы с пользовательскими типами в Java используются различные подходы.

Проблема

JSON не имеет встроенного способа сохранить информацию о типе. Когда мы десериализуем JSON в Java объект, парсер не знает, какой класс использовать.

Решение 1: Jackson с @JsonTypeInfo

// Определение иерархии с type информацией
@JsonTypeInfo(
    use = JsonTypeInfo.Id.NAME,
    include = JsonTypeInfo.As.PROPERTY,
    property = "type"
)
@JsonSubTypes({
    @JsonSubTypes.Type(value = Circle.class, name = "circle"),
    @JsonSubTypes.Type(value = Square.class, name = "square")
})
public abstract class Shape {
    public abstract void draw();
}

public class Circle extends Shape {
    public double radius;
    
    @Override
    public void draw() { System.out.println("Drawing circle"); }
}

public class Square extends Shape {
    public double side;
    
    @Override
    public void draw() { System.out.println("Drawing square"); }
}

// Использование
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(new Circle());
// Результат: {"type":"circle", "radius": 5.0}

Shape shape = mapper.readValue(json, Shape.class);

Решение 2: Кастомный Deserializer

public class ShapeDeserializer extends StdDeserializer<Shape> {
    private static final Map<String, Class<? extends Shape>> typeMap = new HashMap<>();
    
    static {
        typeMap.put("circle", Circle.class);
        typeMap.put("square", Square.class);
    }
    
    public ShapeDeserializer() {
        super(Shape.class);
    }
    
    @Override
    public Shape deserialize(JsonParser p, DeserializationContext ctxt) 
            throws IOException {
        JsonNode node = p.getCodec().readTree(p);
        String type = node.get("type").asText();
        Class<? extends Shape> shapeClass = typeMap.get(type);
        
        if (shapeClass == null) {
            throw new JsonMappingException(p, "Unknown shape type: " + type);
        }
        
        return p.getCodec().treeToValue(node, shapeClass);
    }
}

// Регистрация deserializer
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(Shape.class, new ShapeDeserializer());
mapper.registerModule(module);

Решение 3: Polymorphic deserialization с @JsonCreator

public abstract class Animal {
    public String name;
    
    @JsonCreator
    public static Animal create(@JsonProperty("type") String type, 
                                @JsonAnySetter Map<String, Object> properties) {
        switch (type) {
            case "dog":
                Dog dog = new Dog();
                dog.name = (String) properties.get("name");
                return dog;
            case "cat":
                Cat cat = new Cat();
                cat.name = (String) properties.get("name");
                return cat;
            default:
                throw new IllegalArgumentException("Unknown type: " + type);
        }
    }
}

public class Dog extends Animal {}
public class Cat extends Animal {}

Решение 4: JSON с дополнительными метаданными

// Обёртка для сохранения типа
public class TypedValue<T> {
    public String type;
    public T data;
    
    public TypedValue(T data) {
        this.data = data;
        this.type = data.getClass().getSimpleName();
    }
}

// Использование
Circle circle = new Circle();
TypedValue<Circle> typed = new TypedValue<>(circle);
String json = mapper.writeValueAsString(typed);
// Результат: {"type":"Circle", "data": {...}}

Решение 5: Использование discriminator (OpenAPI/JSON Schema)

@Data
@JsonTypeInfo(
    use = JsonTypeInfo.Id.NAME,
    include = JsonTypeInfo.As.EXISTING_PROPERTY,
    property = "petType",
    defaultImpl = Dog.class,
    visible = true
)
@JsonSubTypes({
    @JsonSubTypes.Type(value = Dog.class, name = "dog"),
    @JsonSubTypes.Type(value = Cat.class, name = "cat")
})
public abstract class Pet {
    public String petType;
    public String name;
}

@Data
public class Dog extends Pet {
    public String breed;
}

@Data
public class Cat extends Pet {
    public int lives;
}

GSON альтернатива

public class GsonTypeAdapter extends TypeAdapter<Shape> {
    @Override
    public Shape read(JsonReader in) throws IOException {
        JsonElement element = JsonParser.parseReader(in);
        String type = element.getAsJsonObject().get("type").getAsString();
        
        switch (type) {
            case "circle":
                return gson.fromJson(element, Circle.class);
            case "square":
                return gson.fromJson(element, Square.class);
            default:
                throw new IllegalArgumentException("Unknown type");
        }
    }
    
    @Override
    public void write(JsonWriter out, Shape shape) throws IOException {
        out.jsonValue(gson.toJson(shape));
    }
}

// Регистрация
Gson gson = new GsonBuilder()
    .registerTypeAdapter(Shape.class, new GsonTypeAdapter())
    .create();

Best Practices

  1. Используй @JsonTypeInfo для простых случаев
  2. Кастомные Deserializers для сложной логики
  3. Не полагайся на порядок полей в JSON
  4. Всегда проверяй тип перед использованием
  5. Версионируй API при изменении типов

Сравнение подходов

ПодходПростотаГибкостьИспользование
@JsonTypeInfo✓✓✓✓✓Рекомендуется
Custom Deserializer✓✓✓Сложные случаи
@JsonCreator✓✓✓✓Альтернатива
Type wrapper✓✓✓Простые типы

Для большинства случаев Jackson с @JsonTypeInfo — это лучший выбор.