How to Use JSON Column Types with Spring Data JPA
Purpose
This post demonstrates how to use JSON column types with Spring Data JPA. JSON columns let you store complex data structures directly in database columns.
Environment
- Spring Boot 3.2.x
- Java 21
- PostgreSQL 15
- Spring Data JPA 3.2.x
The Problem
When building applications, I need flexible data structures. Sometimes I don’t want to create separate database tables for related data. JSON columns solve this problem.
JSON<List<String>> lets me store collections as JSON arrays. No need for join tables or complex relationships.
Database Setup
First, my database must support JSON columns. PostgreSQL works well for this.
-- Create table with JSON columnsCREATE TABLE products ( id BIGSERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, tags JSON, -- JSON array of strings attributes JSONB -- JSON object);
-- Index for JSON queriesCREATE INDEX idx_product_tags ON products USING GIN(tags);CREATE INDEX idx_product_attributes ON products USING GIN(attributes);Entity Mapping
Here’s my entity with JSON fields:
@Entity@Table(name = "products")public class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
private String name;
@Column(columnDefinition = "JSON") private List<String> tags;
@Column(columnDefinition = "JSON") private Map<String, Object> attributes;
// Getters and setters public Long getId() { return id; } public void setId(Long id) { this.id = id; }
public String getName() { return name; } public void setName(String name) { this.name = name; }
public List<String> getTags() { return tags; } public void setTags(List<String> tags) { this.tags = tags; }
public Map<String, Object> getAttributes() { return attributes; } public void setAttributes(Map<String, Object> attributes) { this.attributes = attributes; }}JSON Collections Support
Spring CRUD Generator v1.2.0 adds support for JSON collections. This means I can store lists and sets directly as JSON.
@Repositorypublic interface ProductRepository extends JpaRepository<Product, Long> {
// Find products by tag @Query("SELECT p FROM Product p WHERE JSON_CONTAINS(p.tags, :tag)") List<Product> findByTag(@Param("tag") String tag);
// Find products by JSON attribute @Query("SELECT p FROM Product p WHERE JSON_EXTRACT(p.attributes, '$.color') = :color") List<Product> findByColor(@Param("color") String color);
// Find products with specific variant @Query("SELECT p FROM Product p WHERE JSON_CONTAINS(p.variants, :variantId, '$.id')") List<Product> findByVariantId(@Param("variantId") Long variantId);}Service Layer
My service handles the business logic:
@Servicepublic class ProductService {
@Autowired private ProductRepository productRepository;
public Product createProduct(ProductDTO dto) { Product product = new Product(); product.setName(dto.getName()); product.setTags(dto.getTags()); // `JSON<List<String>>` product.setAttributes(dto.getAttributes()); // `JSON<Map<String, Object>>`
return productRepository.save(product); }
public List<Product> searchByTag(String tag) { return productRepository.findByTag(tag); }}DTO Pattern
I use DTOs to control data exposure:
public class ProductDTO { private String name; private List<String> tags; private Map<String, Object> attributes;
// Nested `JSON<List<ProductVariant>>` private List<ProductVariantDTO> variants;
// Getters and setters public String getName() { return name; } public void setName(String name) { this.name = name; }
public List<String> getTags() { return tags; } public void setTags(List<String> tags) { this.tags = tags; }
public Map<String, Object> getAttributes() { return attributes; } public void setAttributes(Map<String, Object> attributes) { this.attributes = attributes; }
public List<ProductVariantDTO> getVariants() { return variants; } public void setVariants(List<ProductVariantDTO> variants) { this.variants = variants; }}Database Storage Format
When I save data, JSON columns store like this:
-- Tags become JSON array["spring-boot", "jpa", "json", "database"]
-- Attributes become JSON object{ "color": "blue", "size": "large", "weight": 1.5}
-- Nested objects become JSON array of objects[ { "id": 1, "name": "Variant 1", "price": 19.99 }, { "id": 2, "name": "Variant 2", "price": 24.99 }]JSON Conversion
Sometimes I need custom JSON conversion. Here’s how I handle it:
@Converterpublic class JsonConverter implements AttributeConverter<Object, String> {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override public String convertToDatabaseColumn(Object attribute) { try { return objectMapper.writeValueAsString(attribute); } catch (JsonProcessingException e) { throw new RuntimeException("Error converting to JSON", e); } }
@Override public Object convertToEntityAttribute(String dbData) { try { return objectMapper.readValue(dbData, Object.class); } catch (JsonProcessingException e) { throw new RuntimeException("Error parsing JSON", e); } }}When to Use JSON Columns
I use JSON columns for:
- Product attributes that vary by product
- User profiles with dynamic fields
- Configuration settings
- Tag systems
- Feature flags
Performance Considerations
JSON queries can be slow. I index my JSON columns:
CREATE INDEX idx_product_tags ON products USING GIN(tags);I also limit JSON size. Large JSON objects impact performance.
Security
JSON data can contain malicious content. I validate all JSON data before storing it.
// Validate JSON dataif (dto.getTags() != null) { dto.getTags().forEach(tag -> { if (tag == null || tag.trim().isEmpty()) { throw new IllegalArgumentException("Invalid tag"); } });}Summary
In this post, I showed how to use JSON column types with Spring Data JPA. The key point is JSON<List<Type>> stores collections directly in database columns without separate tables.
When implementing JSON columns:
- Choose a database that supports JSON
- Index JSON columns for performance
- Validate JSON data for security
- Use DTOs to control data exposure
- Keep JSON objects reasonable in size
The Spring CRUD Generator v1.2.0 simplifies this with automatic JSON<List<Type>> support. Less boilerplate code, more flexible data structures.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments