Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Debugging Intermittent JSON Parsing Failures with serde

Tech 1

Encountered an odd issue when calling an external API—JSON deserialization worked on the first call but then started failing intermittently. The error message trailing input at line 1 column 690 kept appearing on subsequent calls.

Reproducing the Issue

First invocation succeeded:

>$ cnb news list
iPhone 18 Pro Still Uses Centered Smaller Dynamic Island, Apple AI Pin Expected Next Year
...
[#813396][posted@ [4 hours ago]][views: 13][comments: 0][likes: 0]

Immediately running the same command again:

>$ cnb news list
Error: error decoding response body

Caused by:
    trailing input at line 1 column 690

The parser claims there's extra contant after valid JSON, which seemed impossible.

Examining the Raw JSON Response

Grabbed the raw response from the API:

[{"Id":813396,"Title":"iPhone 18 Pro Still Uses Centered Smaller Dynamic Island...","DateAdded":"2026-01-22T14:18:00+08:00"},...]

The JSON validated correctly online. Both the JSON and parsing code appeared fine, which usual means something else is wrong.

Added debug logging to inspect the raw response and parsing results:

use anyhow::Result;
use reqwest::{Client, Response};
use serde::Serialize;

use crate::{api::urls::OPENAPI, models::news::NewsInfo, tools::IntoAnyhowResult};

pub async fn list_news(c: &Client, page: impl Serialize + Send + Sync) -> Result<Vec<NewsInfo>> {
    let res = raw_list_news(c, page).await?.text().await?;
    println!("res: {}", res);

    Ok(serde_json::from_str(&res)?)
}

pub async fn raw_list_news(c: &Client, page: impl Serialize + Send + Sync) -> Result<Response> {
    let url = format!("{}/{}", OPENAPI, "NewsItems");
    c.get(url).query(&page).send().await.into_anyhow_result()
}
>$ cargo run --bin cnb -- news list --page-index 2 --page-size 12
res: [{"Id":813396,..., "DateAdded":"2026-01-22T14:18:00"},...]
[iPhone 18 Pro Still Uses Centered Smaller Dynamic Island...]
...

>$ cargo run --bin cnb -- news list --page-index 2 --page-size 12
res: [{"Id":813396,..., "DateAdded":"2026-01-22T14:18:00+08:00"},...]
Error: trailing input at line 1 column 690

Both calls returned complete JSON, so the issue wasn't truncated responses.

Inspecting the Struct Definition

use chrono::NaiveDateTime;
use serde::Deserialize;

#[derive(Debug, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
#[serde(default)]
pub struct NewsInfo {
    pub id: u64,
    pub title: String,
    pub summary: String,
    pub topic_id: u64,
    pub topic_icon: String,
    pub view_count: u64,
    pub comment_count: u64,
    pub digg_count: u64,
    pub date_added: NaiveDateTime,
}

Incremental Debugging

Parsed the JSON into serde_json::Value first, then deserialized each item individually:

pub async fn list_news(c: &Client, page: impl Serialize + Send + Sync) -> Result<Vec<NewsInfo>> {
    let res = raw_list_news(c, page).await?.text().await?;
    println!("res: {}", res);

    let values: Vec<serde_json::Value> = serde_json::from_str(&res)?;

    for (i, item) in values.iter().enumerate() {
        if let Err(e) = serde_json::from_value::<NewsInfo>(item.clone()) {
            eprintln!("❌ failed at index {i}: {e}");
            eprintln!("{}", item);
            break;
        }
    }

    Ok(serde_json::from_str(&res)?)
}

Second call error:

 failed at index 0: trailing input
{"CommentCount":0,"DateAdded":"2026-01-22T14:18:00+08:00",...}

The issue surfaced on DateAdded. The struct expects NaiveDateTime (no timezone), but the API returned "2026-01-22T14:18:00+08:00" with timezone information.

First call had:

"DateAdded":"2026-01-22T14:18:00"

Without timezone. The API returns inconsistent date formats between calls.

Changed date_added to String for testing:

 failed at index 6: invalid type: null, expected a string
{"TopicIcon":null,...}

This time TopicIcon was null. The struct expected String, not Option<String>.

The #[serde(default)] attribute only applies when fields are missing entirely. A JSON null maps to Option::None, not the default value.

Final struct fix:

#[derive(Debug, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
#[serde(default)]
pub struct NewsInfo {
    pub id: u64,
    pub title: String,
    pub summary: String,
    pub topic_id: u64,
    pub topic_icon: Option<String>,
    pub view_count: u64,
    pub comment_count: u64,
    pub digg_count: u64,
    pub date_added: String,
}

This "mysterious" parsing failure came down to two API inconsistencies: unstable datetime formats and null values appearing where strings were expected.

Tags: Rustserde

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.