[{"data":1,"prerenderedAt":2269},["ShallowReactive",2],{"doc:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-timestamp-is-not-json-serializable-when-exporting-excel-data":3,"surround:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-timestamp-is-not-json-serializable-when-exporting-excel-data":2233,"ancestors:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-timestamp-is-not-json-serializable-when-exporting-excel-data":2242,"children:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-timestamp-is-not-json-serializable-when-exporting-excel-data":2247,"related:\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-timestamp-is-not-json-serializable-when-exporting-excel-data":2248},{"id":4,"title":5,"body":6,"dateModified":2199,"datePublished":2199,"description":2200,"extension":2201,"faq":2202,"keyword":2211,"kind":2212,"meta":2213,"navigation":119,"path":2222,"seo":2223,"slug":2226,"stem":2227,"topics":2228,"type":2231,"__hash__":2232},"docs\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-timestamp-is-not-json-serializable-when-exporting-excel-data\u002Findex.md","Fix \"Object of type Timestamp is not JSON serializable\" When Exporting Excel Data",{"type":7,"value":8,"toc":2185},"minimark",[9,40,45,73,79,83,311,363,498,502,508,560,591,595,605,939,977,1076,1094,1098,1101,1281,1304,1451,1455,1470,1691,1704,1840,1844,1847,1969,1976,1980,2005,2009,2090,2094,2120,2124,2130,2136,2142,2148,2152,2181],[10,11,12,13,17,18,21,22,25,26,29,30,33,34,39],"p",{},"A common pipeline reads a sheet with pandas and posts the rows to an API, writes them to a message queue, or saves them as a JSON file. The obvious code — ",[14,15,16],"code",{},"json.dumps(df.to_dict(\"records\"))"," — fails on the first date with ",[14,19,20],{},"TypeError: Object of type Timestamp is not JSON serializable",". Fix that, and the next run fails with ",[14,23,24],{},"Object of type int64 is not JSON serializable","; fix that, and the JSON is written but the receiving JavaScript service rejects it because it contains ",[14,27,28],{},"NaN",". All three come from the same mismatch: pandas uses its own and NumPy's types for performance, and Python's ",[14,31,32],{},"json"," module only knows the built-in types. This page shows each failure and three ways to fix it. It belongs to ",[35,36,38],"a",{"href":37},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002F","Troubleshooting Common Python Excel Errors",".",[41,42,44],"h2",{"id":43},"prerequisites","Prerequisites",[46,47,52],"pre",{"className":48,"code":49,"language":50,"meta":51,"style":51},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl\n","bash","",[14,53,54],{"__ignoreMap":51},[55,56,59,63,67,70],"span",{"class":57,"line":58},"line",1,[55,60,62],{"class":61},"sMTad","pip",[55,64,66],{"class":65},"srMev"," install",[55,68,69],{"class":65}," pandas",[55,71,72],{"class":65}," openpyxl\n",[10,74,75,78],{},[14,76,77],{},"visits.xlsx"," has an integer ID, text with accents, a date-time, a time of day, an amount with one blank, and a follow-up date with one blank — a typical mix for a sheet that feeds another system.",[41,80,82],{"id":81},"step-1-reproduce-the-three-failures","Step 1 — Reproduce the three failures",[46,84,88],{"className":85,"code":86,"language":87,"meta":51,"style":51},"language-python shiki shiki-themes github-light github-dark-high-contrast","import json\nimport pandas as pd\n\ndf = pd.read_excel(\"visits.xlsx\")\nprint(df.dtypes.to_dict())\n\nfor label, payload in [(\"records\", df.to_dict(\"records\")),\n                       (\"one scalar\", {\"id\": df.loc[0, \"VisitID\"]})]:\n    try:\n        json.dumps(payload)\n    except TypeError as exc:\n        print(f\"{label:10} TypeError: {exc}\")\n\nprint(json.dumps({\"amount\": df.loc[1, \"Amount\"]}))\n","python",[14,89,90,100,114,121,139,149,154,180,210,219,225,240,282,287],{"__ignoreMap":51},[55,91,92,96],{"class":57,"line":58},[55,93,95],{"class":94},"s-kum","import",[55,97,99],{"class":98},"skGVy"," json\n",[55,101,103,105,108,111],{"class":57,"line":102},2,[55,104,95],{"class":94},[55,106,107],{"class":98}," pandas ",[55,109,110],{"class":94},"as",[55,112,113],{"class":98}," pd\n",[55,115,117],{"class":57,"line":116},3,[55,118,120],{"emptyLinePlaceholder":119},true,"\n",[55,122,124,127,130,133,136],{"class":57,"line":123},4,[55,125,126],{"class":98},"df ",[55,128,129],{"class":94},"=",[55,131,132],{"class":98}," pd.read_excel(",[55,134,135],{"class":65},"\"visits.xlsx\"",[55,137,138],{"class":98},")\n",[55,140,142,146],{"class":57,"line":141},5,[55,143,145],{"class":144},"sP0c6","print",[55,147,148],{"class":98},"(df.dtypes.to_dict())\n",[55,150,152],{"class":57,"line":151},6,[55,153,120],{"emptyLinePlaceholder":119},[55,155,157,160,163,166,169,172,175,177],{"class":57,"line":156},7,[55,158,159],{"class":94},"for",[55,161,162],{"class":98}," label, payload ",[55,164,165],{"class":94},"in",[55,167,168],{"class":98}," [(",[55,170,171],{"class":65},"\"records\"",[55,173,174],{"class":98},", df.to_dict(",[55,176,171],{"class":65},[55,178,179],{"class":98},")),\n",[55,181,183,186,189,192,195,198,201,204,207],{"class":57,"line":182},8,[55,184,185],{"class":98},"                       (",[55,187,188],{"class":65},"\"one scalar\"",[55,190,191],{"class":98},", {",[55,193,194],{"class":65},"\"id\"",[55,196,197],{"class":98},": df.loc[",[55,199,200],{"class":144},"0",[55,202,203],{"class":98},", ",[55,205,206],{"class":65},"\"VisitID\"",[55,208,209],{"class":98},"]})]:\n",[55,211,213,216],{"class":57,"line":212},9,[55,214,215],{"class":94},"    try",[55,217,218],{"class":98},":\n",[55,220,222],{"class":57,"line":221},10,[55,223,224],{"class":98},"        json.dumps(payload)\n",[55,226,228,231,234,237],{"class":57,"line":227},11,[55,229,230],{"class":94},"    except",[55,232,233],{"class":144}," TypeError",[55,235,236],{"class":94}," as",[55,238,239],{"class":98}," exc:\n",[55,241,243,246,249,252,255,259,262,265,268,271,273,276,278,280],{"class":57,"line":242},12,[55,244,245],{"class":144},"        print",[55,247,248],{"class":98},"(",[55,250,251],{"class":94},"f",[55,253,254],{"class":65},"\"",[55,256,258],{"class":257},"sSjpA","{",[55,260,261],{"class":98},"label",[55,263,264],{"class":94},":10",[55,266,267],{"class":257},"}",[55,269,270],{"class":65}," TypeError: ",[55,272,258],{"class":257},[55,274,275],{"class":98},"exc",[55,277,267],{"class":257},[55,279,254],{"class":65},[55,281,138],{"class":98},[55,283,285],{"class":57,"line":284},13,[55,286,120],{"emptyLinePlaceholder":119},[55,288,290,292,295,298,300,303,305,308],{"class":57,"line":289},14,[55,291,145],{"class":144},[55,293,294],{"class":98},"(json.dumps({",[55,296,297],{"class":65},"\"amount\"",[55,299,197],{"class":98},[55,301,302],{"class":144},"1",[55,304,203],{"class":98},[55,306,307],{"class":65},"\"Amount\"",[55,309,310],{"class":98},"]}))\n",[10,312,313,316,317,320,321,323,324,327,328,331,332,335,336,339,340,342,343,346,347,349,350,354,355,358,359,362],{},[14,314,315],{},"to_dict(\"records\")"," hands back ",[14,318,319],{},"Timestamp"," objects for the date-time column, which ",[14,322,32],{}," cannot encode. Pulling a single value out with ",[14,325,326],{},".loc"," gives a NumPy ",[14,329,330],{},"int64",", which fails the same way — ",[14,333,334],{},"to_dict"," happens to convert integers to Python ",[14,337,338],{},"int",", so this one tends to appear in code that builds payloads by hand. The third case does not fail at all: the blank amount is ",[14,341,28],{},", and ",[14,344,345],{},"json.dumps"," writes it as the bare word ",[14,348,28],{},", which is ",[351,352,353],"em",{},"not"," valid JSON. Python's own ",[14,356,357],{},"json.loads"," accepts it; JavaScript's ",[14,360,361],{},"JSON.parse",", most web frameworks and many databases reject the whole document.",[364,365,373,374,373,378,373,382,373,389,373,397,373,404,373,411,373,416,373,422,373,424,373,435,373,438,373,442,373,444,373,449,373,452,373,456,373,458,373,462,373,465,373,469,373,471,373,474,373,477,373,481,373,483,373,486,373,489,373,492,373,494],"svg",{"viewBox":366,"role":367,"ariaLabelledBy":368,"xmlns":371,"style":372},"4 0 752 276","img",[369,370],"json-types-t","json-types-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[375,376,377],"title",{"id":369},"Values from read_excel and what json.dumps does with them",[379,380,381],"desc",{"id":370},"A table of value types produced by pandas from an Excel sheet. Python int, float, str and None encode fine. pandas Timestamp, numpy int64 and Decimal raise TypeError. NaN is written as the bare word NaN, which strict JSON parsers reject. datetime.time raises TypeError.",[383,384],"rect",{"x":385,"y":200,"width":386,"height":387,"fill":388},"4","752","276","#ffffff",[383,390],{"x":391,"y":392,"width":393,"height":394,"rx":200,"fill":388,"stroke":395,"style":396},"20.0","16.0","218.1","40.0","var(--line,#cdd5e6)","stroke-width:1px",[383,398],{"x":399,"y":392,"width":400,"height":394,"rx":200,"fill":401,"stroke":402,"style":403},"238.1","501.9","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:1.2px",[405,406,410],"text",{"x":407,"y":408,"style":409},"489.1","41.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","json.dumps result",[383,412],{"x":391,"y":413,"width":393,"height":414,"rx":200,"fill":415,"stroke":395,"style":396},"56.0","34.0","#e7ebef",[405,417,421],{"x":418,"y":419,"style":420},"32.0","78.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start","int, float, str, None",[383,423],{"x":399,"y":413,"width":400,"height":414,"rx":200,"fill":388,"stroke":395,"style":396},[425,426,428],"g",{"transform":427},"translate(480.1,64.0) scale(0.750)",[429,430],"path",{"d":431,"fill":432,"stroke":433,"style":434},"M5 12.5l4.5 4.5L19 7","none","var(--teal,#0f9488)","stroke-width:2.40px;stroke-linecap:round;stroke-linejoin:round",[383,436],{"x":391,"y":437,"width":393,"height":414,"rx":200,"fill":415,"stroke":395,"style":396},"90.0",[405,439,441],{"x":418,"y":440,"style":420},"112.0","pandas Timestamp",[383,443],{"x":399,"y":437,"width":400,"height":414,"rx":200,"fill":388,"stroke":395,"style":396},[405,445,448],{"x":407,"y":446,"style":447},"111.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","TypeError",[383,450],{"x":391,"y":451,"width":393,"height":414,"rx":200,"fill":415,"stroke":395,"style":396},"124.0",[405,453,455],{"x":418,"y":454,"style":420},"146.0","numpy int64 \u002F float64 scalar",[383,457],{"x":399,"y":451,"width":400,"height":414,"rx":200,"fill":388,"stroke":395,"style":396},[405,459,461],{"x":407,"y":460,"style":447},"145.0","TypeError (int64)",[383,463],{"x":391,"y":464,"width":393,"height":414,"rx":200,"fill":415,"stroke":395,"style":396},"158.0",[405,466,468],{"x":418,"y":467,"style":420},"180.0","datetime.date \u002F datetime.time",[383,470],{"x":399,"y":464,"width":400,"height":414,"rx":200,"fill":388,"stroke":395,"style":396},[405,472,448],{"x":407,"y":473,"style":447},"179.0",[383,475],{"x":391,"y":476,"width":393,"height":414,"rx":200,"fill":415,"stroke":395,"style":396},"192.0",[405,478,480],{"x":418,"y":479,"style":420},"214.0","Decimal",[383,482],{"x":399,"y":476,"width":400,"height":414,"rx":200,"fill":388,"stroke":395,"style":396},[405,484,448],{"x":407,"y":485,"style":447},"213.0",[383,487],{"x":391,"y":488,"width":393,"height":414,"rx":200,"fill":415,"stroke":395,"style":396},"226.0",[405,490,28],{"x":418,"y":491,"style":420},"248.0",[383,493],{"x":399,"y":488,"width":400,"height":414,"rx":200,"fill":388,"stroke":395,"style":396},[405,495,497],{"x":407,"y":496,"style":447},"247.0","NaN — invalid JSON",[41,499,501],{"id":500},"step-2-fix-1-let-pandas-write-the-json","Step 2 — Fix 1: let pandas write the JSON",[10,503,504,507],{},[14,505,506],{},"DataFrame.to_json"," knows every pandas type. Ask for ISO dates and records:",[46,509,511],{"className":85,"code":510,"language":87,"meta":51,"style":51},"text = df.to_json(orient=\"records\", date_format=\"iso\", force_ascii=False)\nprint(text)\n",[14,512,513,553],{"__ignoreMap":51},[55,514,515,518,520,523,527,529,531,533,536,538,541,543,546,548,551],{"class":57,"line":58},[55,516,517],{"class":98},"text ",[55,519,129],{"class":94},[55,521,522],{"class":98}," df.to_json(",[55,524,526],{"class":525},"sa561","orient",[55,528,129],{"class":94},[55,530,171],{"class":65},[55,532,203],{"class":98},[55,534,535],{"class":525},"date_format",[55,537,129],{"class":94},[55,539,540],{"class":65},"\"iso\"",[55,542,203],{"class":98},[55,544,545],{"class":525},"force_ascii",[55,547,129],{"class":94},[55,549,550],{"class":144},"False",[55,552,138],{"class":98},[55,554,555,557],{"class":57,"line":102},[55,556,145],{"class":144},[55,558,559],{"class":98},"(text)\n",[10,561,562,563,566,567,570,571,574,575,578,579,582,583,586,587,590],{},"Timestamps become ",[14,564,565],{},"\"2026-09-01T10:30:00.000\"",", missing values become ",[14,568,569],{},"null",", numbers stay numbers and accented text stays readable with ",[14,572,573],{},"force_ascii=False",". Always pass ",[14,576,577],{},"date_format=\"iso\"",": the default writes dates as milliseconds since 1970 (",[14,580,581],{},"1788258600000","), which the receiver has to know to decode, and pandas 3 warns that this default is going away. Two details to check against what the receiver expects: date-only values from Excel come out as midnight date-times (",[14,584,585],{},"\"2026-09-15T00:00:00.000\"","), and every timestamp carries milliseconds. If the API wants ",[14,588,589],{},"\"2026-09-15\"",", format those columns as strings first (Step 4).",[41,592,594],{"id":593},"step-3-fix-2-give-jsondumps-a-converter","Step 3 — Fix 2: give json.dumps a converter",[10,596,597,598,601,602,604],{},"When the payload is built by hand — nested structures, metadata around the rows — pass a ",[14,599,600],{},"default"," function that ",[14,603,32],{}," calls for any object it does not recognise:",[46,606,608],{"className":85,"code":607,"language":87,"meta":51,"style":51},"import datetime as dt\nimport decimal\nimport math\nimport numpy as np\n\ndef to_json_value(obj):\n    if isinstance(obj, (pd.Timestamp, dt.datetime, dt.date, dt.time)):\n        return obj.isoformat()\n    if isinstance(obj, np.integer):\n        return int(obj)\n    if isinstance(obj, np.floating):\n        return None if math.isnan(obj) else float(obj)\n    if isinstance(obj, decimal.Decimal):\n        return str(obj)\n    if obj is pd.NaT or obj is pd.NA:\n        return None\n    raise TypeError(f\"cannot serialise {type(obj).__name__}\")\n\npayload = {\"source\": \"visits.xlsx\", \"rows\": df.to_dict(\"records\"), \"first_id\": df.loc[0, \"VisitID\"]}\ntry:\n    print(json.dumps(payload, default=to_json_value, ensure_ascii=False, allow_nan=False))\nexcept ValueError as exc:\n    print(\"ValueError:\", exc)\n",[14,609,610,622,629,636,648,652,664,675,683,692,702,711,732,741,750,779,787,819,824,870,878,913,926],{"__ignoreMap":51},[55,611,612,614,617,619],{"class":57,"line":58},[55,613,95],{"class":94},[55,615,616],{"class":98}," datetime ",[55,618,110],{"class":94},[55,620,621],{"class":98}," dt\n",[55,623,624,626],{"class":57,"line":102},[55,625,95],{"class":94},[55,627,628],{"class":98}," decimal\n",[55,630,631,633],{"class":57,"line":116},[55,632,95],{"class":94},[55,634,635],{"class":98}," math\n",[55,637,638,640,643,645],{"class":57,"line":123},[55,639,95],{"class":94},[55,641,642],{"class":98}," numpy ",[55,644,110],{"class":94},[55,646,647],{"class":98}," np\n",[55,649,650],{"class":57,"line":141},[55,651,120],{"emptyLinePlaceholder":119},[55,653,654,657,661],{"class":57,"line":151},[55,655,656],{"class":94},"def",[55,658,660],{"class":659},"s_Opv"," to_json_value",[55,662,663],{"class":98},"(obj):\n",[55,665,666,669,672],{"class":57,"line":156},[55,667,668],{"class":94},"    if",[55,670,671],{"class":144}," isinstance",[55,673,674],{"class":98},"(obj, (pd.Timestamp, dt.datetime, dt.date, dt.time)):\n",[55,676,677,680],{"class":57,"line":182},[55,678,679],{"class":94},"        return",[55,681,682],{"class":98}," obj.isoformat()\n",[55,684,685,687,689],{"class":57,"line":212},[55,686,668],{"class":94},[55,688,671],{"class":144},[55,690,691],{"class":98},"(obj, np.integer):\n",[55,693,694,696,699],{"class":57,"line":221},[55,695,679],{"class":94},[55,697,698],{"class":144}," int",[55,700,701],{"class":98},"(obj)\n",[55,703,704,706,708],{"class":57,"line":227},[55,705,668],{"class":94},[55,707,671],{"class":144},[55,709,710],{"class":98},"(obj, np.floating):\n",[55,712,713,715,718,721,724,727,730],{"class":57,"line":242},[55,714,679],{"class":94},[55,716,717],{"class":144}," None",[55,719,720],{"class":94}," if",[55,722,723],{"class":98}," math.isnan(obj) ",[55,725,726],{"class":94},"else",[55,728,729],{"class":144}," float",[55,731,701],{"class":98},[55,733,734,736,738],{"class":57,"line":284},[55,735,668],{"class":94},[55,737,671],{"class":144},[55,739,740],{"class":98},"(obj, decimal.Decimal):\n",[55,742,743,745,748],{"class":57,"line":289},[55,744,679],{"class":94},[55,746,747],{"class":144}," str",[55,749,701],{"class":98},[55,751,753,755,758,761,764,767,769,771,774,777],{"class":57,"line":752},15,[55,754,668],{"class":94},[55,756,757],{"class":98}," obj ",[55,759,760],{"class":94},"is",[55,762,763],{"class":98}," pd.NaT ",[55,765,766],{"class":94},"or",[55,768,757],{"class":98},[55,770,760],{"class":94},[55,772,773],{"class":98}," pd.",[55,775,776],{"class":144},"NA",[55,778,218],{"class":98},[55,780,782,784],{"class":57,"line":781},16,[55,783,679],{"class":94},[55,785,786],{"class":144}," None\n",[55,788,790,793,795,797,799,802,804,807,810,813,815,817],{"class":57,"line":789},17,[55,791,792],{"class":94},"    raise",[55,794,233],{"class":144},[55,796,248],{"class":98},[55,798,251],{"class":94},[55,800,801],{"class":65},"\"cannot serialise ",[55,803,258],{"class":257},[55,805,806],{"class":144},"type",[55,808,809],{"class":98},"(obj).",[55,811,812],{"class":144},"__name__",[55,814,267],{"class":257},[55,816,254],{"class":65},[55,818,138],{"class":98},[55,820,822],{"class":57,"line":821},18,[55,823,120],{"emptyLinePlaceholder":119},[55,825,827,830,832,835,838,841,843,845,848,851,853,856,859,861,863,865,867],{"class":57,"line":826},19,[55,828,829],{"class":98},"payload ",[55,831,129],{"class":94},[55,833,834],{"class":98}," {",[55,836,837],{"class":65},"\"source\"",[55,839,840],{"class":98},": ",[55,842,135],{"class":65},[55,844,203],{"class":98},[55,846,847],{"class":65},"\"rows\"",[55,849,850],{"class":98},": df.to_dict(",[55,852,171],{"class":65},[55,854,855],{"class":98},"), ",[55,857,858],{"class":65},"\"first_id\"",[55,860,197],{"class":98},[55,862,200],{"class":144},[55,864,203],{"class":98},[55,866,206],{"class":65},[55,868,869],{"class":98},"]}\n",[55,871,873,876],{"class":57,"line":872},20,[55,874,875],{"class":94},"try",[55,877,218],{"class":98},[55,879,881,884,887,889,891,894,897,899,901,903,906,908,910],{"class":57,"line":880},21,[55,882,883],{"class":144},"    print",[55,885,886],{"class":98},"(json.dumps(payload, ",[55,888,600],{"class":525},[55,890,129],{"class":94},[55,892,893],{"class":98},"to_json_value, ",[55,895,896],{"class":525},"ensure_ascii",[55,898,129],{"class":94},[55,900,550],{"class":144},[55,902,203],{"class":98},[55,904,905],{"class":525},"allow_nan",[55,907,129],{"class":94},[55,909,550],{"class":144},[55,911,912],{"class":98},"))\n",[55,914,916,919,922,924],{"class":57,"line":915},22,[55,917,918],{"class":94},"except",[55,920,921],{"class":144}," ValueError",[55,923,236],{"class":94},[55,925,239],{"class":98},[55,927,929,931,933,936],{"class":57,"line":928},23,[55,930,883],{"class":144},[55,932,248],{"class":98},[55,934,935],{"class":65},"\"ValueError:\"",[55,937,938],{"class":98},", exc)\n",[10,940,941,942,944,945,948,949,952,953,955,956,959,960,963,964,966,967,969,970,972,973,976],{},"The converter handles the types that raised ",[14,943,448],{},", but this call still fails — with ",[14,946,947],{},"ValueError: Out of range float values are not JSON compliant: nan"," — because ",[14,950,951],{},"allow_nan=False"," caught the ",[14,954,28],{},". That is the point of the flag: a plain Python ",[14,957,958],{},"float('nan')"," is a ",[351,961,962],{},"known"," type, so ",[14,965,600],{}," is never asked about it, and without ",[14,968,951],{}," it would slip into the output as ",[14,971,28],{},". Decimals are written as strings to keep every digit; switch to ",[14,974,975],{},"float(obj)"," if the receiver wants numbers. To make the hand-built payload pass, replace the missing values before building it:",[46,978,980],{"className":85,"code":979,"language":87,"meta":51,"style":51},"records = df.astype(object).where(df.notna(), None).to_dict(\"records\")\npayload = {\"source\": \"visits.xlsx\", \"rows\": records, \"first_id\": df.loc[0, \"VisitID\"]}\nprint(json.dumps(payload, default=to_json_value, ensure_ascii=False, allow_nan=False)[:160])\n",[14,981,982,1008,1041],{"__ignoreMap":51},[55,983,984,987,989,992,995,998,1001,1004,1006],{"class":57,"line":58},[55,985,986],{"class":98},"records ",[55,988,129],{"class":94},[55,990,991],{"class":98}," df.astype(",[55,993,994],{"class":144},"object",[55,996,997],{"class":98},").where(df.notna(), ",[55,999,1000],{"class":144},"None",[55,1002,1003],{"class":98},").to_dict(",[55,1005,171],{"class":65},[55,1007,138],{"class":98},[55,1009,1010,1012,1014,1016,1018,1020,1022,1024,1026,1029,1031,1033,1035,1037,1039],{"class":57,"line":102},[55,1011,829],{"class":98},[55,1013,129],{"class":94},[55,1015,834],{"class":98},[55,1017,837],{"class":65},[55,1019,840],{"class":98},[55,1021,135],{"class":65},[55,1023,203],{"class":98},[55,1025,847],{"class":65},[55,1027,1028],{"class":98},": records, ",[55,1030,858],{"class":65},[55,1032,197],{"class":98},[55,1034,200],{"class":144},[55,1036,203],{"class":98},[55,1038,206],{"class":65},[55,1040,869],{"class":98},[55,1042,1043,1045,1047,1049,1051,1053,1055,1057,1059,1061,1063,1065,1067,1070,1073],{"class":57,"line":116},[55,1044,145],{"class":144},[55,1046,886],{"class":98},[55,1048,600],{"class":525},[55,1050,129],{"class":94},[55,1052,893],{"class":98},[55,1054,896],{"class":525},[55,1056,129],{"class":94},[55,1058,550],{"class":144},[55,1060,203],{"class":98},[55,1062,905],{"class":525},[55,1064,129],{"class":94},[55,1066,550],{"class":144},[55,1068,1069],{"class":98},")[:",[55,1071,1072],{"class":144},"160",[55,1074,1075],{"class":98},"])\n",[10,1077,1078,1079,1081,1082,1084,1085,1088,1089,342,1091,1093],{},"Now ",[14,1080,600],{}," turns the timestamp and the NumPy integer into JSON-friendly values, the ",[14,1083,28],{}," and ",[14,1086,1087],{},"NaT"," cells are already ",[14,1090,1000],{},[14,1092,951],{}," stays in place as a guard for any value that slips through later.",[41,1095,1097],{"id":1096},"step-4-fix-3-clean-the-frame-before-converting","Step 4 — Fix 3: clean the frame before converting",[10,1099,1100],{},"The most predictable approach is to convert the DataFrame into exactly the shape the receiver wants, then serialise plain Python objects:",[46,1102,1104],{"className":85,"code":1103,"language":87,"meta":51,"style":51},"clean = df.copy()\nclean[\"Visited\"] = clean[\"Visited\"].dt.strftime(\"%Y-%m-%dT%H:%M:%S\")\nclean[\"Follow-up\"] = pd.to_datetime(clean[\"Follow-up\"]).dt.strftime(\"%Y-%m-%d\")\nclean[\"Start\"] = clean[\"Start\"].map(lambda t: t.strftime(\"%H:%M\") if pd.notna(t) else None)\nclean = clean.astype(object).where(clean.notna(), None)\nrows = clean.to_dict(\"records\")\nprint(json.dumps(rows, ensure_ascii=False, allow_nan=False, indent=1))\n",[14,1105,1106,1116,1148,1175,1217,1235,1249],{"__ignoreMap":51},[55,1107,1108,1111,1113],{"class":57,"line":58},[55,1109,1110],{"class":98},"clean ",[55,1112,129],{"class":94},[55,1114,1115],{"class":98}," df.copy()\n",[55,1117,1118,1121,1124,1127,1129,1132,1134,1137,1140,1143,1146],{"class":57,"line":102},[55,1119,1120],{"class":98},"clean[",[55,1122,1123],{"class":65},"\"Visited\"",[55,1125,1126],{"class":98},"] ",[55,1128,129],{"class":94},[55,1130,1131],{"class":98}," clean[",[55,1133,1123],{"class":65},[55,1135,1136],{"class":98},"].dt.strftime(",[55,1138,1139],{"class":65},"\"%Y-%m-",[55,1141,1142],{"class":257},"%d",[55,1144,1145],{"class":65},"T%H:%M:%S\"",[55,1147,138],{"class":98},[55,1149,1150,1152,1155,1157,1159,1162,1164,1167,1169,1171,1173],{"class":57,"line":116},[55,1151,1120],{"class":98},[55,1153,1154],{"class":65},"\"Follow-up\"",[55,1156,1126],{"class":98},[55,1158,129],{"class":94},[55,1160,1161],{"class":98}," pd.to_datetime(clean[",[55,1163,1154],{"class":65},[55,1165,1166],{"class":98},"]).dt.strftime(",[55,1168,1139],{"class":65},[55,1170,1142],{"class":257},[55,1172,254],{"class":65},[55,1174,138],{"class":98},[55,1176,1177,1179,1182,1184,1186,1188,1190,1193,1196,1199,1202,1205,1208,1211,1213,1215],{"class":57,"line":123},[55,1178,1120],{"class":98},[55,1180,1181],{"class":65},"\"Start\"",[55,1183,1126],{"class":98},[55,1185,129],{"class":94},[55,1187,1131],{"class":98},[55,1189,1181],{"class":65},[55,1191,1192],{"class":98},"].map(",[55,1194,1195],{"class":94},"lambda",[55,1197,1198],{"class":98}," t: t.strftime(",[55,1200,1201],{"class":65},"\"%H:%M\"",[55,1203,1204],{"class":98},") ",[55,1206,1207],{"class":94},"if",[55,1209,1210],{"class":98}," pd.notna(t) ",[55,1212,726],{"class":94},[55,1214,717],{"class":144},[55,1216,138],{"class":98},[55,1218,1219,1221,1223,1226,1228,1231,1233],{"class":57,"line":141},[55,1220,1110],{"class":98},[55,1222,129],{"class":94},[55,1224,1225],{"class":98}," clean.astype(",[55,1227,994],{"class":144},[55,1229,1230],{"class":98},").where(clean.notna(), ",[55,1232,1000],{"class":144},[55,1234,138],{"class":98},[55,1236,1237,1240,1242,1245,1247],{"class":57,"line":151},[55,1238,1239],{"class":98},"rows ",[55,1241,129],{"class":94},[55,1243,1244],{"class":98}," clean.to_dict(",[55,1246,171],{"class":65},[55,1248,138],{"class":98},[55,1250,1251,1253,1256,1258,1260,1262,1264,1266,1268,1270,1272,1275,1277,1279],{"class":57,"line":156},[55,1252,145],{"class":144},[55,1254,1255],{"class":98},"(json.dumps(rows, ",[55,1257,896],{"class":525},[55,1259,129],{"class":94},[55,1261,550],{"class":144},[55,1263,203],{"class":98},[55,1265,905],{"class":525},[55,1267,129],{"class":94},[55,1269,550],{"class":144},[55,1271,203],{"class":98},[55,1273,1274],{"class":525},"indent",[55,1276,129],{"class":94},[55,1278,302],{"class":144},[55,1280,912],{"class":98},[10,1282,1283,1286,1287,1084,1289,1291,1292,1294,1295,1297,1298,1300,1301,1303],{},[14,1284,1285],{},"astype(object).where(notna, None)"," replaces every ",[14,1288,28],{},[14,1290,1087],{}," with ",[14,1293,1000],{},", which becomes ",[14,1296,569],{},". Each date column now has exactly the format the receiving system documents, and ",[14,1299,951],{}," passes because no ",[14,1302,28],{}," is left. This is more code than Fix 1, but every field's format is explicit and visible in review — which is what you want when a partner system parses the output.",[364,1305,373,1310,373,1313,373,1316,373,1319,373,1323,373,1329,373,1333,373,1337,373,1340,373,1345,373,1350,373,1354,373,1357,373,1361,373,1365,373,1368,373,1370,373,1374,373,1376,373,1378,373,1381,373,1385,373,1389,373,1392,373,1394,373,1396,373,1399,373,1403,373,1405,373,1407,373,1409,373,1411,373,1413,373,1415,373,1417,373,1419,373,1425,373,1429,373,1434,373,1439,373,1446],{"viewBox":1306,"role":367,"ariaLabelledBy":1307,"xmlns":371,"style":372},"4 0 752 168",[1308,1309],"json-clean-t","json-clean-d",[375,1311,1312],{"id":1308},"A row before and after the cleaning step",[379,1314,1315],{"id":1309},"On the left the DataFrame row with a Timestamp, a NaN amount and a NaT follow-up date. On the right the JSON object with an ISO date-time string, null amount and null follow-up.",[383,1317],{"x":385,"y":200,"width":386,"height":1318,"fill":388},"168",[383,1320],{"x":391,"y":394,"width":1321,"height":1322,"rx":200,"fill":388,"stroke":395,"style":396},"74.2","28.0",[405,1324,1328],{"x":1325,"y":1326,"style":1327},"57.1","59.0","font-size:10.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","field",[383,1330],{"x":1331,"y":394,"width":1332,"height":1322,"rx":200,"fill":388,"stroke":395,"style":396},"94.2","152.2",[405,1334,1336],{"x":1335,"y":1326,"style":1327},"170.3","value",[383,1338],{"x":391,"y":1339,"width":1321,"height":1322,"rx":200,"fill":388,"stroke":395,"style":396},"68.0",[405,1341,1344],{"x":1325,"y":1342,"style":1343},"87.0","font-size:10.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","Visited",[383,1346],{"x":1331,"y":1339,"width":1332,"height":1322,"rx":200,"fill":1347,"stroke":1348,"style":1349},"#fdefd8","var(--gold,#b4740a)","stroke-width:1.5px",[405,1351,1353],{"x":1335,"y":1342,"style":1352},"font-size:10.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Timestamp(2026-09-02)",[383,1355],{"x":391,"y":1356,"width":1321,"height":1322,"rx":200,"fill":388,"stroke":395,"style":396},"96.0",[405,1358,1360],{"x":1325,"y":1359,"style":1343},"115.0","Amount",[383,1362],{"x":1331,"y":1356,"width":1332,"height":1322,"rx":200,"fill":1363,"stroke":1364,"style":1349},"#fce9e9","var(--danger,#dc2626)",[405,1366,28],{"x":1335,"y":1359,"style":1367},"font-size:10.5px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle",[383,1369],{"x":391,"y":451,"width":1321,"height":1322,"rx":200,"fill":388,"stroke":395,"style":396},[405,1371,1373],{"x":1325,"y":1372,"style":1343},"143.0","Follow-up",[383,1375],{"x":1331,"y":451,"width":1332,"height":1322,"rx":200,"fill":1363,"stroke":1364,"style":1349},[405,1377,1087],{"x":1335,"y":1372,"style":1367},[383,1379],{"x":1380,"y":394,"width":1321,"height":1322,"rx":200,"fill":388,"stroke":395,"style":396},"511.2",[405,1382,1384],{"x":1383,"y":1326,"style":1327},"548.3","key",[383,1386],{"x":1387,"y":394,"width":1388,"height":1322,"rx":200,"fill":388,"stroke":395,"style":396},"585.4","154.6",[405,1390,1336],{"x":1391,"y":1326,"style":1327},"662.7",[383,1393],{"x":1380,"y":1339,"width":1321,"height":1322,"rx":200,"fill":388,"stroke":395,"style":396},[405,1395,1344],{"x":1383,"y":1342,"style":1343},[383,1397],{"x":1387,"y":1339,"width":1388,"height":1322,"rx":200,"fill":1398,"stroke":433,"style":1349},"#d9f4f1",[405,1400,1402],{"x":1391,"y":1342,"style":1401},"font-size:10.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","\"2026-09-02T00:00:00\"",[383,1404],{"x":1380,"y":1356,"width":1321,"height":1322,"rx":200,"fill":388,"stroke":395,"style":396},[405,1406,1360],{"x":1383,"y":1359,"style":1343},[383,1408],{"x":1387,"y":1356,"width":1388,"height":1322,"rx":200,"fill":1398,"stroke":433,"style":1349},[405,1410,569],{"x":1391,"y":1359,"style":1401},[383,1412],{"x":1380,"y":451,"width":1321,"height":1322,"rx":200,"fill":388,"stroke":395,"style":396},[405,1414,1373],{"x":1383,"y":1372,"style":1343},[383,1416],{"x":1387,"y":451,"width":1388,"height":1322,"rx":200,"fill":1398,"stroke":433,"style":1349},[405,1418,569],{"x":1391,"y":1372,"style":1401},[405,1420,1424],{"x":1421,"y":1422,"style":1423},"133.2","29.0","font-size:12.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","DataFrame row",[405,1426,1428],{"x":1427,"y":1422,"style":1423},"625.6","JSON",[57,1430],{"x1":1431,"y1":1356,"x2":1432,"y2":1356,"stroke":402,"style":1433},"256.3","497.2","stroke-width:2px",[1435,1436],"polygon",{"points":1437,"fill":1438},"501.2,96.0 492.2,101.0 492.2,91.0","#5b5cf0",[383,1440],{"x":1441,"y":1442,"width":1443,"height":1444,"rx":1445,"fill":388,"stroke":395,"style":403},"288.8","63.0","179.9","22.0","11",[405,1447,1450],{"x":1448,"y":419,"style":1449},"378.8","font-size:11.5px;font-weight:400;fill:var(--brand-strong,#4338ca);text-anchor:middle;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace","clean, then json.dumps",[41,1452,1454],{"id":1453},"step-5-verify-the-output-is-strict-json","Step 5 — Verify the output is strict JSON",[10,1456,1457,1458,1460,1461,1084,1463,1466,1467,1469],{},"Python's ",[14,1459,357],{}," is lenient about ",[14,1462,28],{},[14,1464,1465],{},"Infinity",", so testing with it proves little. Parse with a hook that refuses those constants — it behaves like ",[14,1468,361],{},":",[46,1471,1473],{"className":85,"code":1472,"language":87,"meta":51,"style":51},"def strict_loads(text):\n    def reject(name):\n        raise ValueError(f\"non-standard JSON constant: {name}\")\n    return json.loads(text, parse_constant=reject)\n\nfor label, text in [(\"to_json\", df.to_json(orient=\"records\", date_format=\"iso\")),\n                    (\"cleaned\", json.dumps(rows, allow_nan=False)),\n                    (\"naive\", json.dumps({\"amount\": float(\"nan\")}))]:\n    try:\n        strict_loads(text)\n        print(f\"{label:8} valid strict JSON\")\n    except ValueError as exc:\n        print(f\"{label:8} {exc}\")\n",[14,1474,1475,1485,1496,1521,1537,1541,1574,1593,1618,1624,1629,1653,1663],{"__ignoreMap":51},[55,1476,1477,1479,1482],{"class":57,"line":58},[55,1478,656],{"class":94},[55,1480,1481],{"class":659}," strict_loads",[55,1483,1484],{"class":98},"(text):\n",[55,1486,1487,1490,1493],{"class":57,"line":102},[55,1488,1489],{"class":94},"    def",[55,1491,1492],{"class":659}," reject",[55,1494,1495],{"class":98},"(name):\n",[55,1497,1498,1501,1503,1505,1507,1510,1512,1515,1517,1519],{"class":57,"line":116},[55,1499,1500],{"class":94},"        raise",[55,1502,921],{"class":144},[55,1504,248],{"class":98},[55,1506,251],{"class":94},[55,1508,1509],{"class":65},"\"non-standard JSON constant: ",[55,1511,258],{"class":257},[55,1513,1514],{"class":98},"name",[55,1516,267],{"class":257},[55,1518,254],{"class":65},[55,1520,138],{"class":98},[55,1522,1523,1526,1529,1532,1534],{"class":57,"line":123},[55,1524,1525],{"class":94},"    return",[55,1527,1528],{"class":98}," json.loads(text, ",[55,1530,1531],{"class":525},"parse_constant",[55,1533,129],{"class":94},[55,1535,1536],{"class":98},"reject)\n",[55,1538,1539],{"class":57,"line":141},[55,1540,120],{"emptyLinePlaceholder":119},[55,1542,1543,1545,1548,1550,1552,1555,1558,1560,1562,1564,1566,1568,1570,1572],{"class":57,"line":151},[55,1544,159],{"class":94},[55,1546,1547],{"class":98}," label, text ",[55,1549,165],{"class":94},[55,1551,168],{"class":98},[55,1553,1554],{"class":65},"\"to_json\"",[55,1556,1557],{"class":98},", df.to_json(",[55,1559,526],{"class":525},[55,1561,129],{"class":94},[55,1563,171],{"class":65},[55,1565,203],{"class":98},[55,1567,535],{"class":525},[55,1569,129],{"class":94},[55,1571,540],{"class":65},[55,1573,179],{"class":98},[55,1575,1576,1579,1582,1585,1587,1589,1591],{"class":57,"line":156},[55,1577,1578],{"class":98},"                    (",[55,1580,1581],{"class":65},"\"cleaned\"",[55,1583,1584],{"class":98},", json.dumps(rows, ",[55,1586,905],{"class":525},[55,1588,129],{"class":94},[55,1590,550],{"class":144},[55,1592,179],{"class":98},[55,1594,1595,1597,1600,1603,1605,1607,1610,1612,1615],{"class":57,"line":182},[55,1596,1578],{"class":98},[55,1598,1599],{"class":65},"\"naive\"",[55,1601,1602],{"class":98},", json.dumps({",[55,1604,297],{"class":65},[55,1606,840],{"class":98},[55,1608,1609],{"class":144},"float",[55,1611,248],{"class":98},[55,1613,1614],{"class":65},"\"nan\"",[55,1616,1617],{"class":98},")}))]:\n",[55,1619,1620,1622],{"class":57,"line":212},[55,1621,215],{"class":94},[55,1623,218],{"class":98},[55,1625,1626],{"class":57,"line":221},[55,1627,1628],{"class":98},"        strict_loads(text)\n",[55,1630,1631,1633,1635,1637,1639,1641,1643,1646,1648,1651],{"class":57,"line":227},[55,1632,245],{"class":144},[55,1634,248],{"class":98},[55,1636,251],{"class":94},[55,1638,254],{"class":65},[55,1640,258],{"class":257},[55,1642,261],{"class":98},[55,1644,1645],{"class":94},":8",[55,1647,267],{"class":257},[55,1649,1650],{"class":65}," valid strict JSON\"",[55,1652,138],{"class":98},[55,1654,1655,1657,1659,1661],{"class":57,"line":242},[55,1656,230],{"class":94},[55,1658,921],{"class":144},[55,1660,236],{"class":94},[55,1662,239],{"class":98},[55,1664,1665,1667,1669,1671,1673,1675,1677,1679,1681,1683,1685,1687,1689],{"class":57,"line":284},[55,1666,245],{"class":144},[55,1668,248],{"class":98},[55,1670,251],{"class":94},[55,1672,254],{"class":65},[55,1674,258],{"class":257},[55,1676,261],{"class":98},[55,1678,1645],{"class":94},[55,1680,267],{"class":257},[55,1682,834],{"class":257},[55,1684,275],{"class":98},[55,1686,267],{"class":257},[55,1688,254],{"class":65},[55,1690,138],{"class":98},[10,1692,1693,1694,1697,1698,1700,1701,1703],{},"The ",[14,1695,1696],{},"to_json"," and cleaned outputs pass; the naive ",[14,1699,345],{}," of a ",[14,1702,28],{}," fails exactly as a JavaScript or Go service would. Put this check in the export's tests.",[364,1705,373,1710,373,1713,373,1716,373,1719,373,1725,373,1733,373,1738,373,1742,373,1746,373,1749,373,1752,373,1758,373,1763,373,1768,373,1775,373,1780,373,1784,373,1786,373,1789,373,1793,373,1796,373,1799,373,1805,373,1808,373,1811,373,1813,373,1816,373,1820,373,1823,373,1826,373,1833,373,1837],{"viewBox":1706,"role":367,"ariaLabelledBy":1707,"xmlns":371,"style":372},"4 0 752 272",[1708,1709],"json-choose-t","json-choose-d",[375,1711,1712],{"id":1708},"Which fix to use",[379,1714,1715],{"id":1709},"For a flat list of rows, use to_json with ISO dates. For a payload built by hand around the rows, use json.dumps with a default converter and allow_nan=False. When the receiver specifies exact field formats, clean the frame first and serialise plain Python values.",[383,1717],{"x":385,"y":200,"width":386,"height":1718,"fill":388},"272",[383,1720],{"x":1721,"y":392,"width":1722,"height":1723,"rx":1724,"fill":401,"stroke":402,"style":1433},"240.0","280.0","75.5","12",[425,1726,1728],{"transform":1727},"translate(368.0,29.0) scale(1.000)",[429,1729],{"d":1730,"fill":432,"stroke":1731,"style":1732},"M9 8l-4 4 4 4 M15 8l4 4-4 4","var(--brand-strong,#4338ca)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[405,1734,1737],{"x":1735,"y":1723,"style":1736},"380.0","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","What are you serialising?",[57,1739],{"x1":1735,"y1":1740,"x2":1735,"y2":1741,"stroke":402,"style":1433},"91.5","111.5",[57,1743],{"x1":1744,"y1":1741,"x2":1745,"y2":1741,"stroke":402,"style":1433},"134.7","625.3",[57,1747],{"x1":1744,"y1":1741,"x2":1744,"y2":1748,"stroke":402,"style":1433},"157.5",[1435,1750],{"points":1751,"fill":1438},"134.7,161.5 129.7,152.5 139.7,152.5",[383,1753],{"x":1754,"y":1755,"width":1331,"height":1756,"rx":1757,"fill":388,"stroke":395,"style":403},"87.6","121.5","21.0","10",[405,1759,1762],{"x":1744,"y":1760,"style":1761},"136.0","font-size:11.5px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:middle","just the rows",[383,1764],{"x":391,"y":1765,"width":1766,"height":1767,"rx":1724,"fill":1398,"stroke":433,"style":1433},"163.5","229.3","93.0",[425,1769,1771],{"transform":1770},"translate(122.7,176.5) scale(1.000)",[429,1772],{"d":1773,"fill":432,"stroke":1774,"style":1732},"M3 5h18v14H3z M3 10h18 M3 15h18 M9 5v14","var(--teal-ink,#0b6157)",[405,1776,1779],{"x":1744,"y":1777,"style":1778},"223.0","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","df.to_json(…)",[405,1781,1783],{"x":1744,"y":1782,"style":447},"242.5","orient=records, date_format=iso",[57,1785],{"x1":1735,"y1":1741,"x2":1735,"y2":1748,"stroke":402,"style":1433},[1435,1787],{"points":1788,"fill":1438},"380.0,161.5 375.0,152.5 385.0,152.5",[383,1790],{"x":1791,"y":1755,"width":1792,"height":1756,"rx":1757,"fill":388,"stroke":395,"style":403},"318.9","122.2",[405,1794,1795],{"x":1735,"y":1760,"style":1761},"a custom payload",[383,1797],{"x":1798,"y":1765,"width":1766,"height":1767,"rx":1724,"fill":401,"stroke":402,"style":1433},"265.3",[425,1800,1802],{"transform":1801},"translate(368.0,176.5) scale(1.000)",[429,1803],{"d":1804,"fill":432,"stroke":1731,"style":1732},"M12 3c-4 0-4 1.6-4 3v2h4.2v1H6c-2 0-3 1.4-3 4s1 4 3 4h2v-2.6c0-1.6 1.3-2.9 2.9-2.9h4.2c1.3 0 2.4-1.1 2.4-2.4V6c0-1.6-1.8-3-5.5-3z M10 5.5h.01 M12 21c4 0 4-1.6 4-3v-2h-4.2v-1H18c2 0 3-1.4 3-4s-1-4-3-4h-2v2.6c0 1.6-1.3 2.9-2.9 2.9H8.9c-1.3 0-2.4 1.1-2.4 2.4V18c0 1.6 1.8 3 5.5 3z M14 18.5h.01",[405,1806,1807],{"x":1735,"y":1777,"style":1736},"json.dumps(default=…)",[405,1809,1810],{"x":1735,"y":1782,"style":447},"plus allow_nan=False",[57,1812],{"x1":1745,"y1":1741,"x2":1745,"y2":1748,"stroke":402,"style":1433},[1435,1814],{"points":1815,"fill":1438},"625.3,161.5 620.3,152.5 630.3,152.5",[383,1817],{"x":1818,"y":1755,"width":1819,"height":1756,"rx":1757,"fill":388,"stroke":395,"style":403},"561.8","127.1",[405,1821,1822],{"x":1745,"y":1760,"style":1761},"strict field formats",[383,1824],{"x":1825,"y":1765,"width":1766,"height":1767,"rx":1724,"fill":1347,"stroke":1348,"style":1433},"510.7",[425,1827,1829],{"transform":1828},"translate(613.3,176.5) scale(1.000)",[429,1830],{"d":1831,"fill":432,"stroke":1832,"style":1732},"M3 5h18l-7 8v6l-4 2v-8z","var(--gold-ink,#7a4e06)",[405,1834,1836],{"x":1745,"y":1777,"style":1835},"font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Clean first",[405,1838,1839],{"x":1745,"y":1782,"style":447},"strftime, None for NaN",[41,1841,1843],{"id":1842},"step-6-large-sheets-write-json-lines","Step 6 — Large sheets: write JSON Lines",[10,1845,1846],{},"A single JSON array has to be built in memory and parsed in one go by the receiver. For sheets with hundreds of thousands of rows, JSON Lines — one JSON object per line — is easier on both sides, and pandas writes it directly:",[46,1848,1850],{"className":85,"code":1849,"language":87,"meta":51,"style":51},"df.to_json(\"visits.jsonl\", orient=\"records\", lines=True, date_format=\"iso\", force_ascii=False)\nwith open(\"visits.jsonl\", encoding=\"utf-8\") as fh:\n    lines = fh.readlines()\nfor line in lines:\n    strict_loads(line)\nprint(len(lines), \"lines, each valid JSON\")\n",[14,1851,1852,1896,1925,1935,1947,1952],{"__ignoreMap":51},[55,1853,1854,1857,1860,1862,1864,1866,1868,1870,1873,1875,1878,1880,1882,1884,1886,1888,1890,1892,1894],{"class":57,"line":58},[55,1855,1856],{"class":98},"df.to_json(",[55,1858,1859],{"class":65},"\"visits.jsonl\"",[55,1861,203],{"class":98},[55,1863,526],{"class":525},[55,1865,129],{"class":94},[55,1867,171],{"class":65},[55,1869,203],{"class":98},[55,1871,1872],{"class":525},"lines",[55,1874,129],{"class":94},[55,1876,1877],{"class":144},"True",[55,1879,203],{"class":98},[55,1881,535],{"class":525},[55,1883,129],{"class":94},[55,1885,540],{"class":65},[55,1887,203],{"class":98},[55,1889,545],{"class":525},[55,1891,129],{"class":94},[55,1893,550],{"class":144},[55,1895,138],{"class":98},[55,1897,1898,1901,1904,1906,1908,1910,1913,1915,1918,1920,1922],{"class":57,"line":102},[55,1899,1900],{"class":94},"with",[55,1902,1903],{"class":144}," open",[55,1905,248],{"class":98},[55,1907,1859],{"class":65},[55,1909,203],{"class":98},[55,1911,1912],{"class":525},"encoding",[55,1914,129],{"class":94},[55,1916,1917],{"class":65},"\"utf-8\"",[55,1919,1204],{"class":98},[55,1921,110],{"class":94},[55,1923,1924],{"class":98}," fh:\n",[55,1926,1927,1930,1932],{"class":57,"line":116},[55,1928,1929],{"class":98},"    lines ",[55,1931,129],{"class":94},[55,1933,1934],{"class":98}," fh.readlines()\n",[55,1936,1937,1939,1942,1944],{"class":57,"line":123},[55,1938,159],{"class":94},[55,1940,1941],{"class":98}," line ",[55,1943,165],{"class":94},[55,1945,1946],{"class":98}," lines:\n",[55,1948,1949],{"class":57,"line":141},[55,1950,1951],{"class":98},"    strict_loads(line)\n",[55,1953,1954,1956,1958,1961,1964,1967],{"class":57,"line":151},[55,1955,145],{"class":144},[55,1957,248],{"class":98},[55,1959,1960],{"class":144},"len",[55,1962,1963],{"class":98},"(lines), ",[55,1965,1966],{"class":65},"\"lines, each valid JSON\"",[55,1968,138],{"class":98},[10,1970,1971,1972,1975],{},"Each line is a complete, strict JSON object, so a consumer can stream the file, process rows as they arrive, and resume after a failure at a known line. Most data tools — BigQuery, DuckDB, Elasticsearch bulk loads, ",[14,1973,1974],{},"pd.read_json(lines=True)"," — accept the format natively.",[41,1977,1979],{"id":1978},"performance-notes","Performance notes",[10,1981,1982,1984,1985,1987,1988,1990,1991,1084,1994,1997,1998,2000,2001,2004],{},[14,1983,1696],{}," is implemented in C and serialises hundreds of thousands of rows per second; ",[14,1986,345],{}," with a ",[14,1989,600],{}," function calls back into Python for every Timestamp and NumPy value and is several times slower on large frames. The cleaning approach in Step 4 sits in between: the vectorised ",[14,1992,1993],{},"strftime",[14,1995,1996],{},"where"," calls are fast, and ",[14,1999,345],{}," then only sees built-in types. For very large exports, prefer ",[14,2002,2003],{},"to_json(lines=True)"," written straight to a file over building one giant string in memory.",[41,2006,2008],{"id":2007},"common-pitfalls","Common pitfalls",[2010,2011,2012,2025,2037,2055,2070],"ul",{},[2013,2014,2015,2021,2022,2024],"li",{},[2016,2017,2018,2019,39],"strong",{},"Testing with ",[14,2020,357],{}," It accepts ",[14,2023,28],{},"; use a strict check.",[2013,2026,2027,2030,2031,2033,2034,2036],{},[2016,2028,2029],{},"Relying on the epoch default."," ",[14,2032,1696],{}," without ",[14,2035,577],{}," writes milliseconds and warns in pandas 3.",[2013,2038,2039,2047,2048,2051,2052,39],{},[2016,2040,2041,2042,1084,2044,39],{},"Scalars from ",[14,2043,326],{},[14,2045,2046],{},".iloc"," They are NumPy types; convert with ",[14,2049,2050],{},".item()"," or ",[14,2053,2054],{},"int()",[2013,2056,2057,2060,2061,2064,2065,2051,2068,39],{},[2016,2058,2059],{},"Time-of-day values."," openpyxl returns ",[14,2062,2063],{},"datetime.time"," for time-formatted cells; they need ",[14,2066,2067],{},"isoformat()",[14,2069,1993],{},[2013,2071,2072,2030,2075,2078,2079,2082,2083,2085,2086,2089],{},[2016,2073,2074],{},"Text encoding.",[14,2076,2077],{},"ensure_ascii=True"," (the default) escapes ",[14,2080,2081],{},"ü"," as ",[14,2084,2081],{},"; valid, but pass ",[14,2087,2088],{},"ensure_ascii=False"," and write UTF-8 if people read the file.",[41,2091,2093],{"id":2092},"conclusion","Conclusion",[10,2095,2096,2098,2099,203,2101,1084,2103,2105,2106,2109,2110,2113,2114,2116,2117,2119],{},[14,2097,345],{}," only understands Python's built-in types, and rows read from Excel contain pandas Timestamps, NumPy scalars, ",[14,2100,2063],{},[14,2102,480],{},[14,2104,28],{},". Use ",[14,2107,2108],{},"df.to_json(orient=\"records\", date_format=\"iso\")"," for plain row lists, a ",[14,2111,2112],{},"default="," converter with ",[14,2115,951],{}," for hand-built payloads, or an explicit cleaning step when the receiver specifies formats — and verify the result with a strict parser, because Python's own reader accepts ",[14,2118,28],{}," that other systems reject.",[41,2121,2123],{"id":2122},"frequently-asked-questions","Frequently asked questions",[10,2125,2126,2129],{},[2016,2127,2128],{},"How do I fix \"Object of type Timestamp is not JSON serializable\"?","\nUse df.to_json(orient=\"records\", date_format=\"iso\"), or pass a default function to json.dumps that returns obj.isoformat() for Timestamp, datetime, date and time objects.",[10,2131,2132,2135],{},[2016,2133,2134],{},"Why does json.dumps say \"Object of type int64 is not JSON serializable\"?","\nThe value is a NumPy integer, typically taken from a DataFrame with .loc or .iloc. Convert it with int(value) or value.item(), or handle np.integer in a default function.",[10,2137,2138,2141],{},[2016,2139,2140],{},"Why does my exported JSON contain NaN, and why is it a problem?","\njson.dumps writes float NaN as the bare word NaN, which is not valid JSON, so JavaScript's JSON.parse and many APIs reject the document. Replace NaN with None before serialising, and pass allow_nan=False so any leftover NaN raises an error.",[10,2143,2144,2147],{},[2016,2145,2146],{},"What date format does DataFrame.to_json use?","\nBy default it writes milliseconds since 1970, a default pandas 3 deprecates. Pass date_format=\"iso\" to get ISO 8601 strings such as 2026-09-01T10:30:00.000.",[41,2149,2151],{"id":2150},"related","Related",[2010,2153,2154,2160,2167,2174],{},[2013,2155,2156,2157,2159],{},"Up one level: ",[35,2158,38],{"href":37}," — common failures and their fixes.",[2013,2161,2162,2166],{},[35,2163,2165],{"href":2164},"\u002Fgetting-started-with-python-excel-automation\u002Fhandling-excel-file-formats-and-conversions\u002Fconvert-an-excel-sheet-to-json-with-python\u002F","Convert an Excel Sheet to JSON with Python"," — the full conversion, including nested shapes.",[2013,2168,2169,2173],{},[35,2170,2172],{"href":2171},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Ffetch-api-data-into-excel-with-python-requests\u002F","Fetch API Data into Excel with Python Requests"," — the opposite direction.",[2013,2175,2176,2180],{},[35,2177,2179],{"href":2178},"\u002Fgetting-started-with-python-excel-automation\u002Fwriting-dataframes-to-excel-with-pandas\u002Fhow-pandas-data-types-are-written-to-excel-cells\u002F","How pandas Data Types Are Written to Excel Cells"," — the same type questions for Excel output.",[2182,2183,2184],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}",{"title":51,"searchDepth":102,"depth":102,"links":2186},[2187,2188,2189,2190,2191,2192,2193,2194,2195,2196,2197,2198],{"id":43,"depth":102,"text":44},{"id":81,"depth":102,"text":82},{"id":500,"depth":102,"text":501},{"id":593,"depth":102,"text":594},{"id":1096,"depth":102,"text":1097},{"id":1453,"depth":102,"text":1454},{"id":1842,"depth":102,"text":1843},{"id":1978,"depth":102,"text":1979},{"id":2007,"depth":102,"text":2008},{"id":2092,"depth":102,"text":2093},{"id":2122,"depth":102,"text":2123},{"id":2150,"depth":102,"text":2151},"2026-10-02","json.dumps fails on rows read from Excel because pandas hands you Timestamp, numpy int64 and NaN values that the json module does not understand — and NaN that it does write produces invalid JSON. Fix it with to_json and ISO dates, a small default= converter, or a clean conversion step, and check the output parses in strict JSON readers.","md",[2203,2205,2207,2209],{"q":2128,"a":2204},"Use df.to_json(orient=\"records\", date_format=\"iso\"), or pass a default function to json.dumps that returns obj.isoformat() for Timestamp, datetime, date and time objects.",{"q":2134,"a":2206},"The value is a NumPy integer, typically taken from a DataFrame with .loc or .iloc. Convert it with int(value) or value.item(), or handle np.integer in a default function.",{"q":2140,"a":2208},"json.dumps writes float NaN as the bare word NaN, which is not valid JSON, so JavaScript's JSON.parse and many APIs reject the document. Replace NaN with None before serialising, and pass allow_nan=False so any leftover NaN raises an error.",{"q":2146,"a":2210},"By default it writes milliseconds since 1970, a default pandas 3 deprecates. Pass date_format=\"iso\" to get ISO 8601 strings such as 2026-09-01T10:30:00.000.","fix timestamp is not json serializable excel",null,{"breadcrumb":2214},[2215,2218,2221],{"name":2216,"item":2217},"Home","\u002F",{"name":2219,"item":2220},"Getting Started with Python Excel Automation","\u002Fgetting-started-with-python-excel-automation\u002F",{"name":38,"item":37},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-timestamp-is-not-json-serializable-when-exporting-excel-data",{"title":2224,"description":2225},"Fix \"Timestamp is not JSON serializable\" for Excel Data","Why json.dumps fails on DataFrame rows from Excel (Timestamp, numpy int64, Decimal, time) and writes invalid NaN; fixes with df.to_json(date_format=\"iso\"), a default= function, converting to Python types, and allow_nan=False checks.","fix-timestamp-is-not-json-serializable-when-exporting-excel-data","getting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-timestamp-is-not-json-serializable-when-exporting-excel-data\u002Findex",[2229,2230],"troubleshooting","file-formats","how-to","XDOfnLaSfND0CjgpR51jeBzQvVKnpekrQhsGkS7oEJ4",[2234,2238],{"title":2235,"path":2236,"stem":2237,"children":-1},"Fix \"This sheet is too large\" When Writing Excel with pandas","\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-this-sheet-is-too-large-error","getting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-this-sheet-is-too-large-error\u002Findex",{"title":2239,"path":2240,"stem":2241,"children":-1},"Fix UnicodeDecodeError When Reading CSV Files Saved by Excel","\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-unicodedecodeerror-when-reading-csv-files-saved-by-excel","getting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors\u002Ffix-unicodedecodeerror-when-reading-csv-files-saved-by-excel\u002Findex",[2243,2245],{"path":2244,"title":2219},"\u002Fgetting-started-with-python-excel-automation",{"path":2246,"title":38},"\u002Fgetting-started-with-python-excel-automation\u002Ftroubleshooting-common-python-excel-errors",[],[2249,2253,2256,2259,2262,2265],{"path":2250,"title":2251,"kind":2252},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fhandle-permission-denied-when-writing-excel-in-python","Handle Permission Denied When Writing Excel in Python","fix",{"path":2254,"title":2255,"kind":2231},"\u002Fgetting-started-with-python-excel-automation\u002Fusing-openpyxl-for-excel-file-manipulation\u002Fwhat-openpyxl-loses-when-saving-an-existing-workbook","What openpyxl Loses When You Save an Existing Workbook",{"path":2257,"title":2258,"kind":2252},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Ffix-excel-serial-numbers-showing-instead-of-dates","Fix Excel Serial Numbers Showing Instead of Dates",{"path":2260,"title":2261,"kind":2231},"\u002Fadvanced-data-transformation-and-cleaning\u002Fworking-with-dates-and-times-in-excel-data\u002Fhandle-daylight-saving-time-in-excel-timestamps","Handle Daylight Saving Time in Excel Timestamps",{"path":2263,"title":2264,"kind":2231},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Fhandle-microsoft-graph-throttling-for-excel","Handle Microsoft Graph Throttling in Excel Automation",{"path":2266,"title":2267,"kind":2268},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation","Error Handling and Logging in Excel Automation","guide",1790972067530]