A demonstration of JSON field order preservation issues in PostgreSQL and a solution using LinkedHashMap in Rust.
This project was created to address a JSON field order preservation issue encountered in the Penumbra Explorer Backend. The problem occurs because PostgreSQL's JSONB format optimizes for querying efficiency rather than preserving exact textual representation, which can cause problems when field order is important.
When storing JSON in PostgreSQL using the JSONB type, the key order in JSON objects is not preserved. This is because JSONB is optimized for querying and analysis rather than preserving the exact textual representation of the JSON.
Original JSON:
{
"title": "Inception",
"genre": "Sci-Fi",
"locations": ["Cinema City Berlin", "Movieplex Hamburg"]
}Retrieved from PostgreSQL:
{
"genre": "Sci-Fi",
"locations": ["Cinema City Berlin", "Movieplex Hamburg"],
"title": "Inception"
}This project demonstrates two approaches:
- Standard approach (using regular JSON serialization) - order not preserved
- Order-preserving approach (using
LinkedHashMap) - order preserved exactly as specified
- Rust with
serde,sqlx, andlinked-hash-map - PostgreSQL for database storage
- Docker for containerization and easy setup
- Docker Compose for orchestration
├── Cargo.toml # Rust dependencies
├── Dockerfile # Rust app container setup
├── docker-compose.yml # Service configuration
├── init.sql # Database initialization
├── json.txt # Custom JSON input file
├── run.sh # Colorful execution script
└── src/
└── main.rs # Rust implementation
- Docker and Docker Compose
- Basic knowledge of Rust and PostgreSQL
-
Clone this repository
git clone https://github.com/yourusername/postgresql-json-order-demo cd postgresql-json-order-demo -
Create a
json.txtfile with your desired JSON data (optional - a sample will be created if none exists) -
Make the run script executable and run it
chmod +x run.sh ./run.sh
The application:
- Creates a PostgreSQL table with both JSONB and raw text columns
- Reads JSON from the
json.txtfile - Stores the JSON in both formats in the database
- Retrieves and displays both versions, showing the order difference
==========================================
JSON Order Preservation Test
==========================================
➤ Original JSON:
{
"movies": [
{
"title": "Inception",
"director": "Christopher Nolan",
"year": 2010,
"genre": "Sci-Fi",
"locations": ["Cinema City Berlin", "Movieplex Hamburg"]
}
]
}
➤ Retrieved JSONB (order not preserved):
{
"movies": [
{
"genre": "Sci-Fi",
"locations": ["Cinema City Berlin", "Movieplex Hamburg"],
"year": 2010,
"title": "Inception",
"director": "Christopher Nolan"
}
]
}
➤ Retrieved Raw Text (exactly as inserted):
{
"movies": [
{
"title": "Inception",
"director": "Christopher Nolan",
"year": 2010,
"genre": "Sci-Fi",
"locations": ["Cinema City Berlin", "Movieplex Hamburg"]
}
]
}
This project supports reading JSON from a file rather than using hardcoded values:
- Create a file named
json.txtin the project root directory - Add your custom JSON data to the file
- Run the application with
./run.sh
The solution uses:
serdeandserde_jsonfor JSON serialization/deserializationlinked-hash-map::LinkedHashMapto preserve field order- Custom structs that use
LinkedHashMapfor field storage - Explicit field addition in the desired order
To use this approach in your own projects:
-
Add necessary dependencies:
serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" linked-hash-map = "0.5"
-
Create structs similar to
MovieOrderedusingLinkedHashMap:struct MovieOrdered { data: LinkedHashMap<String, Value>, } impl MovieOrdered { fn new() -> Self { Self { data: LinkedHashMap::new(), } } fn add(&mut self, key: &str, value: Value) -> &mut Self { self.data.insert(key.to_string(), value); self } }
-
When creating objects, add fields in the exact order you want to preserve:
let mut movie = MovieOrdered::new(); movie.add("title", json!("Inception")) .add("director", json!("Christopher Nolan")) .add("year", json!(2010)) .add("genre", json!("Sci-Fi"));
-
Serialize and store in the database