{"id":16774,"date":"2026-09-09T12:40:46","date_gmt":"2026-09-09T10:40:46","guid":{"rendered":"https:\/\/www.lukaswojcik.com\/blog\/toolbox\/ga4-measurement-protocol-json-validator\/"},"modified":"2026-09-09T13:09:25","modified_gmt":"2026-09-09T11:09:25","slug":"ga4-measurement-protocol-json-validator","status":"publish","type":"page","link":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/ga4-measurement-protocol-json-validator\/","title":{"rendered":"GA4 Measurement Protocol JSON Validator: see every fault at once"},"content":{"rendered":"<div class=\"gtm-analyser-container\" style=\"background: var(--bg-panel, #1e1e24); padding: 25px; border-radius: 8px; border: 1px solid var(--border, #2a2a35);\">\n<p style=\"color: var(--text-secondary, #a0a0b0); margin-bottom: 20px;\">The Measurement Protocol answers 2xx to almost everything. A malformed event, a reserved parameter name, a value one character over the limit \u2014 the request is accepted, the data is dropped, and the report simply stays empty. Google\u2019s validation server helps, but it reports only the first fault it finds, and in the relaxed mode that production actually uses it reports far less than that. This check reads the payload here first and lists everything at once, then shows what the validation server says about the same text in both modes.<\/p>\n<div style=\"margin-bottom: 6px; display: flex; flex-wrap: wrap; gap: 10px; align-items: baseline; justify-content: space-between;\">\n        <label for=\"mpv-json\" style=\"color: var(--text-secondary, #a0a0b0); font-size: 0.85rem;\">Measurement Protocol payload (the JSON POST body)<\/label><br \/>\n        <span style=\"display: flex; gap: 8px;\"><br \/>\n            <button type=\"button\" id=\"mpv-bsp1\" class=\"button\" style=\"background: transparent; color: var(--text-secondary, #a0a0b0); border: 1px solid var(--border, #2a2a35); padding: 5px 12px; border-radius: 6px; font-size: 0.78rem; cursor: pointer;\">Faulty example<\/button><br \/>\n            <button type=\"button\" id=\"mpv-bsp2\" class=\"button\" style=\"background: transparent; color: var(--text-secondary, #a0a0b0); border: 1px solid var(--border, #2a2a35); padding: 5px 12px; border-radius: 6px; font-size: 0.78rem; cursor: pointer;\">Clean example<\/button><br \/>\n        <\/span>\n    <\/div>\n<p>    <textarea id=\"mpv-json\" rows=\"16\" spellcheck=\"false\" style=\"width: 100%; padding: 12px; background: var(--bg-body, #14141a); border: 1px solid var(--border, #2a2a35); color: var(--text-primary, #e8e8ee); border-radius: 6px; box-sizing: border-box; font-family: monospace; font-size: 0.85rem; line-height: 1.5; resize: vertical;\"><\/textarea><\/p>\n<div style=\"display: flex; flex-wrap: wrap; gap: 18px; align-items: center; margin: 14px 0 6px;\">\n        <span><br \/>\n            <label for=\"mpv-strom\" style=\"color: var(--text-secondary, #a0a0b0); font-size: 0.85rem; margin-right: 8px;\">Data stream<\/label><br \/>\n            <select id=\"mpv-strom\" style=\"padding: 7px 10px; background: var(--bg-body, #14141a); border: 1px solid var(--border, #2a2a35); color: var(--text-primary, #e8e8ee); border-radius: 6px;\"><option value=\"web\">Web (client_id)<\/option><option value=\"app\">App (app_instance_id)<\/option><\/select><br \/>\n        <\/span><br \/>\n        <label style=\"color: var(--text-secondary, #a0a0b0); font-size: 0.85rem; cursor: pointer;\"><br \/>\n            <input type=\"checkbox\" id=\"mpv-360\" style=\"vertical-align: middle; margin-right: 6px;\">Analytics 360 property (500-character values)<br \/>\n        <\/label><br \/>\n        <label style=\"color: var(--text-secondary, #a0a0b0); font-size: 0.85rem; cursor: pointer;\"><br \/>\n            <input type=\"checkbox\" id=\"mpv-google\" checked style=\"vertical-align: middle; margin-right: 6px;\">Also ask Google\u2019s validation server<br \/>\n        <\/label>\n    <\/div>\n<p style=\"color: var(--text-secondary, #a0a0b0); font-size: 0.82rem; margin: 0 0 16px;\">No credentials are asked for and none are needed: the validation server checks neither measurement_id nor api_secret, so this tool sends placeholders. The payload itself is not stored \u2014 only its byte count lands in the rate counter. Events sent to the validation server never appear in any report.<\/p>\n<p>    <button id=\"mpv-btn\" class=\"button\" style=\"background: var(--accent, #7ee787); color: var(--on-accent, #0b1114); border: none; padding: 12px 24px; border-radius: 6px; font-weight: 700; cursor: pointer;\">Validate payload<\/button><\/p>\n<div id=\"mpv-ausgabe\" style=\"margin-top: 22px;\"><\/div>\n<p style=\"color: var(--text-secondary, #a0a0b0); font-size: 0.8rem; margin: 24px 0 0;\">Limits worth knowing: the reserved names, size caps and recommended parameters follow the published reference and can change without notice. Recommended-parameter lists are curated for the common events, so an unknown event name draws no recommendation rather than a false one. Whether a property actually collects a parameter depends on its custom dimension setup, which no payload can show.<\/p>\n<\/div>\n<p><script>\n(function () {\n    'use strict';<\/p>\n<p>    const T = {\"einleitung\":\"The Measurement Protocol answers 2xx to almost everything. A malformed event, a reserved parameter name, a value one character over the limit \u2014 the request is accepted, the data is dropped, and the report simply stays empty. Google\u2019s validation server helps, but it reports only the first fault it finds, and in the relaxed mode that production actually uses it reports far less than that. This check reads the payload here first and lists everything at once, then shows what the validation server says about the same text in both modes.\",\"label_json\":\"Measurement Protocol payload (the JSON POST body)\",\"label_strom\":\"Data stream\",\"strom_web\":\"Web (client_id)\",\"strom_app\":\"App (app_instance_id)\",\"label_360\":\"Analytics 360 property (500-character values)\",\"label_google\":\"Also ask Google\u2019s validation server\",\"bsp_fehler\":\"Faulty example\",\"bsp_sauber\":\"Clean example\",\"knopf\":\"Validate payload\",\"laeuft\":\"Reading payload...\",\"datenschutz\":\"No credentials are asked for and none are needed: the validation server checks neither measurement_id nor api_secret, so this tool sends placeholders. The payload itself is not stored \u2014 only its byte count lands in the rate counter. Events sent to the validation server never appear in any report.\",\"grenze\":\"Limits worth knowing: the reserved names, size caps and recommended parameters follow the published reference and can change without notice. Recommended-parameter lists are curated for the common events, so an unknown event name draws no recommendation rather than a false one. Whether a property actually collects a parameter depends on its custom dimension setup, which no payload can show.\",\"h_ereignisse\":\"Events\",\"h_befunde\":\"Findings\",\"h_google\":\"Google\u2019s validation server\",\"sp_nr\":\"No.\",\"sp_name\":\"Event name\",\"sp_parameter\":\"Params\",\"sp_items\":\"Items\",\"sp_urteil\":\"Verdict\",\"zeile_gut\":\"in order\",\"zeile_warnung\":\"warning\",\"zeile_kritisch\":\"faulty\",\"wert_fehlt\":\"no name\",\"zeile_groesse\":\"Payload size\",\"zeile_ereignisse\":\"Events\",\"zeile_parameter\":\"Parameters\",\"zeile_items\":\"Items\",\"zeile_up\":\"User properties\",\"zeile_dauer\":\"Took\",\"zeile_limit\":\"Checks left this hour\",\"einheit_byte\":\"bytes\",\"tausendertrenner\":\",\",\"stufe_hoch\":\"High\",\"stufe_mittel\":\"Medium\",\"stufe_niedrig\":\"Low\",\"stufe_info\":\"Information\",\"stufe_gut\":\"In order\",\"keine_befunde\":\"Nothing to report.\",\"urteil_gut\":\"The payload matches the documented rules.\",\"urteil_warnung\":\"It will be accepted, but parts of it will not arrive as intended.\",\"urteil_kritisch\":\"Something in here is rejected or silently discarded.\",\"google_streng\":\"With validation_behavior: ENFORCE_RECOMMENDATIONS\",\"google_locker\":\"Without validation_behavior \u2014 the way production treats it\",\"google_keine\":\"No message. This check found {eigene} thing(s) worth reporting in the same text.\",\"google_anzahl\":\"{n} message(s). This check found {eigene} thing(s) worth reporting in the same text.\",\"google_uebersprungen\":\"Not asked \u2014 the payload does not parse as JSON, so there is nothing to send.\",\"google_aus\":\"The validation server was not asked for this run.\",\"google_fehler\":\"The validation server could not be reached.\",\"google_fehler_netz\":\"The validation server could not be reached.\",\"google_fehler_status\":\"The validation server answered with an unexpected status.\",\"google_fehler_antwort\":\"The answer from the validation server could not be read.\",\"google_fehler_kein_json\":\"Nothing was sent \u2014 the payload does not parse as JSON.\",\"google_hinweis\":\"The validation server stops at the first fault it finds, and the two runs do not always stop at the same one. Both were sent with placeholder credentials, because the server validates neither measurement_id nor api_secret.\",\"hinweis_komma\":\"There is a comma before a closing bracket \u2014 JSON does not allow a trailing one.\",\"hinweis_anfuehrung\":\"Single quotes appear where JSON requires double ones.\",\"hinweis_schluessel_ohne_anfuehrung\":\"A key appears without quotes. JSON is not JavaScript: every key needs double quotes.\",\"hinweis_klammer\":\"The number of opening and closing braces does not match.\",\"hinweis_unbekannt\":\"No obvious cause \u2014 the position in the message is the place to start.\",\"fehler_kein_token\":\"The check could not be started because reCAPTCHA did not load.\",\"fehler_captcha\":\"reCAPTCHA classified the request as automated. Reloading the page normally helps.\",\"fehler_zu_viele\":\"The limit of {limit} checks per hour for this address has been reached.\",\"fehler_zaehler\":\"The rate counter is unavailable, so nothing was checked.\",\"fehler_pruefdienst\":\"The reCAPTCHA service could not be reached.\",\"fehler_aufbau\":\"The service is not configured correctly. The fault is on this side.\",\"fehler_eingabe\":\"The request could not be read.\",\"fehler_methode\":\"Wrong request method.\",\"fehler_netz\":\"The service could not be reached.\",\"fehler_antwort\":\"The answer could not be read.\",\"fehler_zu_gross\":\"The payload is longer than {grenze} bytes and was not read.\",\"fehler_nichts_eingegeben\":\"Nothing was entered.\",\"fehler_unbekannt\":\"Something went wrong that has no message of its own.\",\"befund_leer\":\"The field is empty || There is nothing to check.\",\"befund_json_kaputt\":\"The payload is not valid JSON || {meldung}. {rat} Nothing else was checked: a payload that does not parse never reaches the Measurement Protocol at all.\",\"befund_kein_objekt\":\"The payload is not an object || A {typ} was found at the top level. The Measurement Protocol expects a JSON object with client_id and events.\",\"befund_bom\":\"The text starts with a byte order mark || Some editors add it invisibly. Strict JSON parsers reject the whole document because of it.\",\"befund_groesse_ueber_grenze\":\"The payload is {bytes} bytes || The documented cap is {grenze}. Beyond it the request is refused as a whole, not trimmed.\",\"befund_groesse\":\"Payload size: {bytes} bytes || That is {anteil}% of the {grenze}-byte cap.\",\"befund_datalayer_statt_mp\":\"This looks like a dataLayer object, not a Measurement Protocol payload || The top level carries \\u0022event\\u0022: \\u0022{wert}\\u0022. In the Measurement Protocol the event name sits in events[].name, and the top level carries client_id.\",\"befund_gtag_statt_mp\":\"This looks like a gtag call, not a Measurement Protocol payload || A key named event_name appears at the top level. The Measurement Protocol expects an events array instead.\",\"befund_zugangsdaten_im_rumpf\":\"Credentials sit in the body || measurement_id and api_secret belong in the query string of the request, not in the JSON. In the body they are unknown fields, and an unknown field makes Google reject the whole payload.\",\"befund_unbekannter_schluessel\":\"Unknown key at the top level: {schluessel} || The Measurement Protocol rejects the entire payload over one unknown field, not just that field. Measured against the validation server, the answer is a parse error without a field path.\",\"befund_client_id_fehlt\":\"client_id is missing || Without it no event can be attributed to a user, and the validation server answers VALUE_REQUIRED.\",\"befund_client_id_typ\":\"client_id is a {typ}, not a string || Even a purely numeric client_id belongs in quotes. Without them Google fails to parse the whole payload.\",\"befund_client_id_leer\":\"client_id is empty || An empty string counts as missing.\",\"befund_client_id_gut\":\"client_id has the recommended form || {wert} \u2014 two numbers joined by a period, exactly what gtag hands out.\",\"befund_client_id_cookie\":\"client_id is the full cookie value || {wert}. The Measurement Protocol accepts it, and this is the documented alternative. The shorter form without the GA1 prefix is what gtag itself reports.\",\"befund_client_id_form\":\"client_id has an unusual form || {wert}. Accepted, but a value that does not come from the browser creates a new user for every request \u2014 that is how a report fills with one-event sessions.\",\"befund_client_id_im_app\":\"client_id in an app payload || App streams identify the installation through app_instance_id. client_id is ignored here.\",\"befund_app_id_im_web\":\"app_instance_id in a web payload || Web streams identify the browser through client_id. app_instance_id is ignored here.\",\"befund_app_id_fehlt\":\"app_instance_id is missing || App streams need it; it comes from the Firebase SDK and is not a client_id.\",\"befund_app_id_typ\":\"app_instance_id is a {typ}, not a string || The value belongs in quotes.\",\"befund_app_id_gut\":\"app_instance_id has the expected form || 32 hexadecimal characters.\",\"befund_app_id_form\":\"app_instance_id has an unusual form || {wert}, {n} characters. The SDK returns 32 hexadecimal characters; anything else usually means a client_id was pasted in.\",\"befund_user_id_typ\":\"user_id is a {typ}, not a string || The value belongs in quotes.\",\"befund_user_id_lang\":\"user_id is {n} characters long || The documented cap is {grenze}.\",\"befund_user_id_pii\":\"user_id looks like personal data ({art}) || Google Analytics forbids sending personal identifiers. A pseudonymous key from the CRM does the same job without the risk of the property being deleted.\",\"befund_zeitstempel_typ\":\"timestamp_micros is not a number || {wert} of type {typ}. Expected is a Unix timestamp in microseconds.\",\"befund_zeitstempel_sekunden\":\"timestamp_micros is in seconds, not microseconds || {wert}. The value is about a million times too small, which puts the event in 1970. The validation server does report this, but as \\u0022too far in the past\\u0022 \u2014 a message that sends people looking for a backdating problem that is not there.\",\"befund_zeitstempel_millisekunden\":\"timestamp_micros is in milliseconds, not microseconds || {wert}. Date.now() returns milliseconds; the Measurement Protocol wants that value times 1000.\",\"befund_zeitstempel_zu_alt\":\"timestamp_micros lies {stunden} hours in the past || Events can be backdated by at most {grenze} hours. Older ones are discarded.\",\"befund_zeitstempel_zukunft\":\"timestamp_micros lies {minuten} minutes in the future || Usually a clock that is out of step, or a time zone added twice.\",\"befund_zeitstempel_gut\":\"timestamp_micros is within the accepted window || About {stunden} hour(s) in the past.\",\"befund_ereignis_zeit_typ\":\"Event {nr}: timestamp_micros is not a number || {wert} of type {typ}.\",\"befund_ereignis_zeit_sekunden\":\"Event {nr}: timestamp_micros is in seconds || {wert}. Microseconds are expected here as well.\",\"befund_ereignis_zeit_millisekunden\":\"Event {nr}: timestamp_micros is in milliseconds || {wert}. The value needs to be multiplied by 1000.\",\"befund_ereignis_zeit_zu_alt\":\"Event {nr}: timestamp_micros lies {stunden} hours in the past || Beyond {grenze} hours the event is discarded.\",\"befund_ereignis_zeit_zukunft\":\"Event {nr}: timestamp_micros lies {minuten} minutes in the future || Usually a clock that is out of step.\",\"befund_ereignis_zeit_gut\":\"Event {nr}: timestamp_micros is within the accepted window || About {stunden} hour(s) in the past.\",\"befund_consent_kein_objekt\":\"consent is a {typ}, not an object || Expected is an object with ad_user_data and ad_personalization.\",\"befund_consent_unbekannt\":\"Unknown key in consent: {schluessel} || Only ad_user_data and ad_personalization exist here. analytics_storage and ad_storage belong to Consent Mode in the browser, not to the Measurement Protocol \u2014 and an unknown key makes Google reject the whole payload.\",\"befund_consent_wert\":\"consent.{schluessel} carries {wert} || Only GRANTED and DENIED are valid. Anything else is an unknown enum value, and the payload fails to parse.\",\"befund_consent_gut\":\"consent is set with {n} valid value(s) || Without consent, Google falls back to the state from the browser interactions of the same client.\",\"befund_npa_veraltet\":\"non_personalized_ads is deprecated || The field still works, but ad_personalization inside consent replaces it and is the one that is read going forward.\",\"befund_verhalten_streng\":\"validation_behavior is ENFORCE_RECOMMENDATIONS || Right for a test. In production this field is better left out, because strict validation rejects events that relaxed validation would still collect.\",\"befund_verhalten_locker\":\"validation_behavior is RELAXED || That is the default anyway. Relaxed validation ignores parameters that exceed the limits instead of reporting them.\",\"befund_verhalten_unbekannt\":\"validation_behavior carries {wert} || Only RELAXED and ENFORCE_RECOMMENDATIONS exist. Anything else is an unknown enum value, and the whole payload fails to parse.\",\"befund_up_kein_objekt\":\"user_properties is a {typ}, not an object || Expected is an object of names, each holding an object with a value key.\",\"befund_up_zu_viele\":\"{n} user properties || At most {grenze} are accepted per request.\",\"befund_up_reserviert\":\"user_properties carries the reserved name {name} || Reserved names are refused. A user ID belongs at the top level as user_id, not in user_properties.\",\"befund_up_praefix\":\"user_properties name {name} starts with {praefix} || That prefix is reserved.\",\"befund_up_name_lang\":\"user_properties name {name} is {n} characters || The cap here is {grenze}, tighter than for event parameters.\",\"befund_up_ohne_value\":\"user_properties {name} has no value wrapper || Expected is {\\u0022value\\u0022: ...}. A bare string makes the whole payload fail to parse.\",\"befund_up_wert_typ\":\"user_properties {name} carries a {typ} as its value || User property values are single values, not structures.\",\"befund_up_wert_lang\":\"The value of {name} is {n} characters || The cap here is {grenze}, tighter than for event parameters.\",\"befund_up_pii\":\"The value of {name} looks like personal data ({art}) || Google Analytics forbids sending personal identifiers.\",\"befund_up_fremder_schluessel\":\"user_properties {name} carries an extra key: {schluessel} || Only value belongs in there.\",\"befund_up_gut\":\"All {n} user properties are within the limits || Names up to 24 characters, values up to 36.\",\"befund_events_fehlt\":\"events is missing || The array is required. The validation server accepts a payload without it without a word \u2014 measured, not assumed \u2014 and nothing is collected.\",\"befund_events_kein_array\":\"events is a {typ}, not an array || Even a single event belongs in an array.\",\"befund_events_leer\":\"events is empty || An empty array carries nothing. The validation server reports no problem, and nothing arrives.\",\"befund_events_zu_viele\":\"{n} events in one request || At most {grenze} are documented. In a measurement against the validation server, 30 events passed without a word, so the excess is dropped quietly rather than reported.\",\"befund_ereignis_kein_objekt\":\"Event {nr} is a {typ}, not an object || Each entry needs name and, optionally, params.\",\"befund_ereignis_fremder_schluessel\":\"Event {nr} carries an unknown key: {schluessel} || Only name and params belong in an event. A timestamp goes into params as timestamp_micros.\",\"befund_ereignis_ohne_name\":\"Event {nr} has no name || Without a name there is nothing to record.\",\"befund_ereignis_name_typ\":\"The name of event {nr} is a {typ} || Expected is a string.\",\"befund_ereignis_name_lang\":\"The name of event {nr} is {n} characters || {name}. The cap is {grenze}.\",\"befund_ereignis_name_form\":\"The name of event {nr} has an invalid form || {name}. Names must start with a letter and may contain only letters, digits and underscores. Hyphens, dots and spaces are refused.\",\"befund_ereignis_name_gross\":\"The name of event {nr} contains capitals || {name}. Event names are case sensitive: Purchase and purchase are two different events, and only the lowercase one feeds the e-commerce reports.\",\"befund_ereignis_name_reserviert\":\"The name of event {nr} is reserved || {name}. Google uses this name itself; the event is refused.\",\"befund_ereignis_name_praefix\":\"The name of event {nr} starts with {praefix} || {name}. That prefix is reserved.\",\"befund_ereignis_nur_app\":\"Event {nr} is only allowed on app streams || {name}. On a web stream it is not collected. The validation server says nothing about it \u2014 measured, not assumed.\",\"befund_ereignis_doppelt\":\"The event name {name} appears {n} times || Two events of the same name in one request are accepted and both counted. Whether that is intended is worth a second look, because it usually comes from a loop that fires once too often.\",\"befund_params_kein_objekt\":\"params of event {nr} is a {typ}, not an object || Parameters are name and value pairs.\",\"befund_params_zu_viele\":\"Event {nr} has {n} parameters || At most {grenze} are accepted per event, and beyond that the request is refused under strict validation.\",\"befund_param_reserviert\":\"Event {nr} uses the reserved parameter name {name} || The name is refused.\",\"befund_param_praefix\":\"Event {nr}: parameter {name} starts with {praefix} || Reserved prefixes are _, firebase_, ga_, google_ and gtag. The parameter is refused.\",\"befund_param_name_lang\":\"Event {nr}: parameter name {name} is {n} characters || The cap is {grenze}.\",\"befund_param_verschachtelt\":\"Event {nr}: parameter {name} carries a {typ} || Only items may hold an array. Every other parameter takes a single value; a nested structure is dropped.\",\"befund_param_null\":\"Event {nr}: parameter {name} is null || A null value is not a value. Leaving the parameter out says the same thing more clearly.\",\"befund_param_leer\":\"Event {nr}: parameter {name} is empty || An empty string looks like an answer and is none. In reports it shows up as an empty dimension value rather than as missing.\",\"befund_param_wert_lang\":\"Event {nr}: the value of {name} is {n} characters || The cap is {grenze}. Under relaxed validation \u2014 the default, and what production uses \u2014 the value is truncated or dropped without a word; measured against the validation server, the same payload came back clean.\",\"befund_param_typ\":\"Event {nr}: {name} should be {soll} but is {ist} || {wert}. Values of the wrong type are refused for known parameters.\",\"befund_param_pii\":\"Event {nr}: {name} looks like personal data ({art}) || Google Analytics forbids sending personal identifiers, and a property that does it can be deleted.\",\"befund_waehrung_form\":\"Event {nr}: currency is not a three-letter code || {wert}. Expected is ISO 4217 in capitals, for example EUR or USD.\",\"befund_wert_ohne_waehrung\":\"Event {nr} has value but no currency || Revenue without a currency is not counted at all. This is the single most common reason for an e-commerce report that stays at zero while the events arrive.\",\"befund_waehrung_ohne_wert\":\"Event {nr} has currency but no value || The currency alone carries no revenue.\",\"befund_session_id_fehlt\":\"No event carries session_id || Without it the events belong to no session. They still show up in some reports, but sessions, engaged sessions and Realtime do not add up.\",\"befund_engagement_fehlt\":\"No event carries engagement_time_msec || Without it the average engagement time stays at zero and the events may be missing from Realtime.\",\"befund_items_kein_array\":\"Event {nr}: items is a {typ}, not an array || Even a single item belongs in an array.\",\"befund_items_leer\":\"Event {nr}: items is empty || An e-commerce event without items fills no item report.\",\"befund_items_zu_viele\":\"Event {nr}: {n} items || At most {grenze} are documented per event.\",\"befund_item_kein_objekt\":\"Event {nr}, item {pos} is a {typ} || Each item is an object with at least item_id or item_name.\",\"befund_item_ohne_kennung\":\"Event {nr}, item {pos} has neither item_id nor item_name || One of the two is required; without either the item cannot be shown.\",\"befund_item_typ\":\"Event {nr}, item {pos}: {feld} is a {ist} || {wert}. Expected is a number. The validation server does check item types \u2014 a price in quotes came back as VALUE_INVALID.\",\"befund_item_eigene_felder\":\"Event {nr}, item {pos} carries {n} custom fields || At most {grenze} item-scoped custom parameters are read per item.\",\"befund_summe_weicht_ab\":\"Event {nr}: value is {value}, the items add up to {summe} || Not an error in itself \u2014 shipping, tax and discounts legitimately explain a gap. Where they do not, one of the two numbers is wrong, and the reports will disagree with the shop.\",\"befund_summe_passt\":\"Event {nr}: value matches the items || Price times quantity adds up to {summe}.\",\"befund_items_fehlen\":\"Event {nr} ({ereignis}) has no items || This event fills the item reports, and without items there is nothing to fill them with.\",\"befund_empfohlene_fehlen\":\"Event {nr} ({ereignis}) is missing recommended parameters || {felder}. The event is collected without them, but the reports built on it stay empty.\",\"befund_empfohlene_da\":\"Event {nr} ({ereignis}) carries the recommended parameters || Nothing missing for this event.\",\"befund_zahlen\":\"{ereignisse} event(s), {parameter} parameter(s), {items} item(s), {user_properties} user propert(ies) || That is what the payload carries.\"};<\/p>\n<p>    var ENDPUNKT = '\/lw-mpvalidate.php';\n    var SITEKEY = '6LcrPkEtAAAAAPo1QCOf-IIM2fCL0UfdJz4y2iSY';\n    var STUFEN = ['hoch', 'mittel', 'niedrig', 'info', 'gut'];\n    var NL = String.fromCharCode(10);<\/p>\n<p>    var BEISPIEL_FEHLER = [\n        '{',\n        '  \"client_id\": \"1234567890.1700000000\",',\n        '  \"timestamp_micros\": 1700000000,',\n        '  \"consent\": { \"ad_user_data\": \"yes\" },',\n        '  \"user_properties\": { \"user_id\": { \"value\": \"k-9910\" } },',\n        '  \"events\": [',\n        '    {',\n        '      \"name\": \"Purchase\",',\n        '      \"params\": {',\n        '        \"transaction_id\": \"T-8891\",',\n        '        \"value\": 25,',\n        '        \"ga_source\": \"newsletter\",',\n        '        \"customer_mail\": \"anna@example.com\",',\n        '        \"items\": [ { \"item_id\": \"A-1\", \"price\": 10, \"quantity\": 2 } ]',\n        '      }',\n        '    },',\n        '    { \"name\": \"screen_view\", \"params\": { \"screen_name\": \"Checkout\" } }',\n        '  ]',\n        '}'\n    ].join(NL);<\/p>\n<p>    var BEISPIEL_SAUBER = [\n        '{',\n        '  \"client_id\": \"1234567890.1700000000\",',\n        '  \"consent\": { \"ad_user_data\": \"GRANTED\", \"ad_personalization\": \"DENIED\" },',\n        '  \"user_properties\": { \"customer_tier\": { \"value\": \"gold\" } },',\n        '  \"events\": [',\n        '    {',\n        '      \"name\": \"purchase\",',\n        '      \"params\": {',\n        '        \"transaction_id\": \"T-8891\",',\n        '        \"currency\": \"EUR\",',\n        '        \"value\": 20,',\n        '        \"session_id\": \"1700000000\",',\n        '        \"engagement_time_msec\": 100,',\n        '        \"items\": [ { \"item_id\": \"A-1\", \"item_name\": \"Shirt\", \"price\": 10, \"quantity\": 2 } ]',\n        '      }',\n        '    }',\n        '  ]',\n        '}'\n    ].join(NL);<\/p>\n<p>    function liste(w) { return Array.isArray(w) ? w : []; }\n    function zahl(w) { return (typeof w === 'number' && isFinite(w)) ? w : null; }\n    function zeichen(w) { return (typeof w === 'string' && w !== '') ? w : null; }\n    function text(schluessel, daten) {\n        var t = T[schluessel];\n        if (typeof t !== 'string') { return null; }\n        return t.replace(\/\\{([a-z_0-9]+)\\}\/g, function (m, k) {\n            return (daten && daten[k] !== undefined && daten[k] !== null) ? String(daten[k]) : m;\n        });\n    }\n    \/** Zahl mit Tausenderpunkten in der Sprache der Seite. *\/\n    function tausend(n) {\n        return String(n).replace(\/\\B(?=(\\d{3})+(?!\\d))\/g, T.tausendertrenner || ' ');\n    }\n    \/** Eine Meldung des Pruefservers auf Tabellenbreite kuerzen. *\/\n    function kurz(s, max) {\n        var t = String(s || '');\n        var m = max || 150;\n        return t.length <= m ? t : t.slice(0, m - 1) + '\u2026';\n    }\n\n    function meldungen(w) {\n        return liste(w).map(function (m) {\n            return { feld: String((m &#038;&#038; m.feld) || ''), code: String((m &#038;&#038; m.code) || ''), meldung: String((m &#038;&#038; m.meldung) || '') };\n        });\n    }\n\n    function bewerten(antwort) {\n        var a = antwort || {};\n        if (a.fehler) { return { fehler: String(a.fehler), limit: zahl(a.limit), grenze: zahl(a.grenze) }; }\n        var befunde = liste(a.befunde).map(function (f) {\n            return { key: String(f.key || ''), stufe: STUFEN.indexOf(f.stufe) === -1 ? 'info' : f.stufe, daten: (f.daten &#038;&#038; typeof f.daten === 'object') ? f.daten : {} };\n        });\n        var gruppen = {};\n        STUFEN.forEach(function (s) { gruppen[s] = befunde.filter(function (f) { return f.stufe === s; }); });\n        var ereignisse = liste(a.ereignisse).map(function (z) {\n            return {\n                nr: zahl(z.nr), name: zeichen(z.name), parameter: zahl(z.parameter), items: zahl(z.items),\n                urteil: (z.urteil === 'gut' || z.urteil === 'warnung') ? z.urteil : 'kritisch'\n            };\n        });\n        var g = (a.google &#038;&#038; typeof a.google === 'object') ? a.google : null;\n        var zahlen = (a.zahlen &#038;&#038; typeof a.zahlen === 'object') ? a.zahlen : {};\n        return {\n            fehler: null,\n            urteil: (a.urteil === 'gut' || a.urteil === 'warnung') ? a.urteil : 'kritisch',\n            bytes: zahl(a.bytes), strom: a.strom === 'app' ? 'app' : 'web',\n            befunde: befunde, gruppen: gruppen, ereignisse: ereignisse,\n            zahlen: {\n                ereignisse: zahl(zahlen.ereignisse) || 0, parameter: zahl(zahlen.parameter) || 0,\n                items: zahl(zahlen.items) || 0, user_properties: zahl(zahlen.user_properties) || 0\n            },\n            google: g ? {\n                streng: g.streng ? { fehler: g.streng.fehler || null, meldungen: meldungen(g.streng.meldungen), dauer: zahl(g.streng.dauer_ms) } : null,\n                locker: g.locker ? { fehler: g.locker.fehler || null, meldungen: meldungen(g.locker.meldungen), dauer: zahl(g.locker.dauer_ms) } : null\n            } : null,\n            dauer: zahl(a.dauer_ms), limitRest: zahl(a.limit_rest)\n        };\n    }\n\n    function skriptLaden() {\n        return new Promise(function (auf) {\n            if (window.grecaptcha) { auf(true); return; }\n            var s = document.createElement('script');\n            s.src = 'https:\/\/www.google.com\/recaptcha\/api.js?render=' + SITEKEY;\n            s.onload = function () { auf(true); }; s.onerror = function () { auf(false); };\n            document.head.appendChild(s);\n            setTimeout(function () { auf(!!window.grecaptcha); }, 8000);\n        });\n    }\n    function tokenHolen() {\n        return skriptLaden().then(function (da) {\n            if (!da || !window.grecaptcha || !window.grecaptcha.ready) { return null; }\n            return new Promise(function (auf) {\n                var fertig = false;\n                function einmal(w) { if (!fertig) { fertig = true; auf(w); } }\n                setTimeout(function () { einmal(null); }, 12000);\n                try {\n                    window.grecaptcha.ready(function () {\n                        if (fertig) { return; }\n                        if (!window.grecaptcha.execute) { einmal(null); return; }\n                        window.grecaptcha.execute(SITEKEY, { action: 'mpvalidate' }).then(function (t) { einmal(t || null); }, function () { einmal(null); });\n                    });\n                } catch (e) { einmal(null); }\n            });\n        }, function () { return null; });\n    }\n    function abfragen(eingabe) {\n        return tokenHolen().then(function (token) {\n            var rumpf = {\n                nutzdaten: eingabe.nutzdaten, strom: eingabe.strom, tarif: eingabe.tarif,\n                google: eingabe.google, token: token || ''\n            };\n            return fetch(ENDPUNKT, { method: 'POST', headers: { 'Content-Type': 'application\/json' }, body: JSON.stringify(rumpf) })\n                .then(function (r) { return r.json().then(function (j) { return j; }, function () { return { fehler: 'antwort' }; }); }, function () { return { fehler: 'netz' }; });\n        });\n    }\n\n    window.LW_TEST = window.LW_TEST || {};\n    window.LW_TEST.MPV = {\n        bewerten: bewerten, darstellen: null, text: text, kurz: kurz, tausend: tausend,\n        ENDPUNKT: ENDPUNKT, STUFEN: STUFEN, T: T,\n        BEISPIEL_FEHLER: BEISPIEL_FEHLER, BEISPIEL_SAUBER: BEISPIEL_SAUBER\n    };\n\n    var btn = document.getElementById('mpv-btn');\n    var out = document.getElementById('mpv-ausgabe');\n    var feld = document.getElementById('mpv-json');\n    if (!btn || !out || !feld) { return; }\n\n    function el(tag, stil, txt) { var e = document.createElement(tag); if (stil) { e.setAttribute('style', stil); } if (txt !== undefined &#038;&#038; txt !== null) { e.textContent = txt; } return e; }\n    var UEBERSCHRIFT = 'font-family: \"Nunito Sans\", sans-serif; font-weight: 700; color: var(--text-primary, #e8e8ee); font-size: 0.95rem; margin: 22px 0 8px;';\n    var ZELLE = 'padding: 5px 12px 5px 0; color: var(--text-secondary, #a0a0b0); font-size: 0.87rem;';\n    var ZELLE_WERT = 'padding: 5px 12px 5px 0; color: var(--text-primary, #e8e8ee); font-size: 0.87rem; font-family: monospace; word-break: break-all;';\n    var KASTEN = 'background: var(--bg-body, #14141a); border: 1px solid var(--border, #2a2a35); border-radius: 8px; padding: 14px 16px; margin: 0 0 10px;';\n    var GUT = ' color: #7ee787;', WARN = ' color: #ffa94d;', ROT = ' color: #ff7b72;', GRAU = ' color: #8a8a99;';\n    var FARBEN = { hoch: '#ff7b72', mittel: '#ffa94d', niedrig: '#e3b341', info: '#8a8a99', gut: '#7ee787' };\n    var URTEIL = { gut: GUT, warnung: WARN, kritisch: ROT };\n    function gitter(sp) { return el('div', 'display: grid; grid-template-columns: ' + sp + '; gap: 0 18px; align-items: baseline;'); }\n    function paar(g, n, w, stil) { g.appendChild(el('div', ZELLE, n)); g.appendChild(el('div', ZELLE_WERT + (stil || ''), w)); }\n\n    function tabelle(b) {\n        var huelle = el('div', 'overflow-x: auto; margin: 0 0 6px;');\n        var t = el('table', 'border-collapse: collapse; width: 100%; min-width: 520px; font-size: 0.84rem;');\n        var kopf = el('tr', '');\n        [T.sp_nr, T.sp_name, T.sp_parameter, T.sp_items, T.sp_urteil].forEach(function (n) {\n            kopf.appendChild(el('th', 'text-align: left; padding: 6px 12px 6px 0; color: #8a8a99; font-weight: 700; font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.4px; border-bottom: 1px solid var(--border, #2a2a35); white-space: nowrap;', n));\n        });\n        t.appendChild(kopf);\n        b.ereignisse.forEach(function (z) {\n            var r = el('tr', '');\n            function zelle(txt, stil) { r.appendChild(el('td', 'padding: 7px 12px 7px 0; border-bottom: 1px solid var(--border, #2a2a35); font-family: monospace; color: var(--text-primary, #e8e8ee);' + (stil || ''), txt)); }\n            zelle(String(z.nr !== null ? z.nr : '-'), GRAU);\n            zelle(z.name || T.wert_fehlt, z.name ? '' : ROT);\n            zelle(String(z.parameter !== null ? z.parameter : '-'), GRAU);\n            zelle(z.items === null ? '-' : String(z.items), GRAU);\n            zelle(T['zeile_' + z.urteil], URTEIL[z.urteil]);\n            t.appendChild(r);\n        });\n        huelle.appendChild(t);\n        return huelle;\n    }\n\n    function googleBlock(name, lauf, eigene) {\n        var kasten = el('div', KASTEN);\n        kasten.appendChild(el('div', 'color: var(--text-primary, #e8e8ee); font-size: 0.9rem; font-weight: 700;', name));\n        if (!lauf) { kasten.appendChild(el('div', ZELLE + ' padding: 6px 0 0;', T.google_uebersprungen)); return kasten; }\n        if (lauf.fehler) {\n            kasten.appendChild(el('div', ZELLE + WARN + ' padding: 6px 0 0;', T['google_fehler_' + lauf.fehler] || T.google_fehler));\n            return kasten;\n        }\n        if (!lauf.meldungen.length) {\n            kasten.appendChild(el('div', ZELLE + GUT + ' padding: 6px 0 0;', text('google_keine', { eigene: eigene })));\n            return kasten;\n        }\n        kasten.appendChild(el('div', ZELLE + ' padding: 6px 0 2px;', text('google_anzahl', { n: lauf.meldungen.length, eigene: eigene })));\n        lauf.meldungen.forEach(function (m) {\n            var z = el('div', 'margin: 8px 0 0; padding: 0 0 0 12px; border-left: 2px solid ' + FARBEN.mittel + ';');\n            z.appendChild(el('div', ZELLE_WERT + ' padding: 0;', m.code + (m.feld ? '  \u00b7  ' + m.feld : '')));\n            z.appendChild(el('div', ZELLE + ' padding: 2px 0 0; line-height: 1.5;', kurz(m.meldung, 320)));\n            kasten.appendChild(z);\n        });\n        return kasten;\n    }\n\n    function darstellen(b, ziel) {\n        ziel.innerHTML = '';\n        if (b.fehler) {\n            var txt = T['fehler_' + b.fehler] || T.fehler_unbekannt;\n            if (b.limit !== null) { txt = txt.replace('{limit}', String(b.limit)); }\n            if (b.grenze !== null) { txt = txt.replace('{grenze}', tausend(b.grenze)); }\n            ziel.appendChild(el('p', ZELLE + WARN + ' margin: 0;', txt));\n            return;\n        }\n        ziel.appendChild(el('div', 'font-size: 1.05rem; font-weight: 700; margin: 0 0 12px;' + URTEIL[b.urteil], T['urteil_' + b.urteil]));\n\n        var g = gitter('auto auto');\n        paar(g, T.zeile_groesse, tausend(b.bytes === null ? 0 : b.bytes) + ' ' + T.einheit_byte, GRAU);\n        paar(g, T.zeile_ereignisse, String(b.zahlen.ereignisse), GRAU);\n        paar(g, T.zeile_parameter, String(b.zahlen.parameter), GRAU);\n        if (b.zahlen.items) { paar(g, T.zeile_items, String(b.zahlen.items), GRAU); }\n        if (b.zahlen.user_properties) { paar(g, T.zeile_up, String(b.zahlen.user_properties), GRAU); }\n        if (b.dauer !== null) { paar(g, T.zeile_dauer, b.dauer + ' ms', GRAU); }\n        if (b.limitRest !== null) { paar(g, T.zeile_limit, String(b.limitRest), GRAU); }\n        ziel.appendChild(g);\n\n        if (b.ereignisse.length) {\n            ziel.appendChild(el('div', UEBERSCHRIFT, T.h_ereignisse));\n            ziel.appendChild(tabelle(b));\n        }\n\n        ziel.appendChild(el('div', UEBERSCHRIFT, T.h_befunde));\n        var irgendwas = false;\n        STUFEN.forEach(function (s) {\n            var gr = b.gruppen[s];\n            if (!gr.length) { return; }\n            irgendwas = true;\n            ziel.appendChild(el('div', 'font-weight: 700; font-size: 0.82rem; text-transform: uppercase; letter-spacing: 0.5px; margin: 14px 0 6px; color: ' + FARBEN[s] + ';', T['stufe_' + s] + ' (' + gr.length + ')'));\n            gr.forEach(function (f) {\n                var k = el('div', KASTEN + ' border-left: 3px solid ' + FARBEN[s] + ';');\n                var daten = {};\n                Object.keys(f.daten).forEach(function (n) {\n                    daten[n] = (n === 'bytes' || n === 'grenze') ? tausend(f.daten[n]) : f.daten[n];\n                });\n                if (f.key === 'json_kaputt') { daten.rat = T['hinweis_' + f.daten.hinweis] || T.hinweis_unbekannt; }\n                var t = text('befund_' + f.key, daten) || f.key;\n                var teile = t.split(' || ');\n                k.appendChild(el('div', 'color: var(--text-primary, #e8e8ee); font-size: 0.9rem; font-weight: 700;', teile[0]));\n                if (teile[1]) { k.appendChild(el('div', ZELLE + ' padding: 6px 0 0; line-height: 1.55;', teile[1])); }\n                ziel.appendChild(k);\n            });\n        });\n        if (!irgendwas) { ziel.appendChild(el('p', ZELLE + ' margin: 0;', T.keine_befunde)); }\n\n        ziel.appendChild(el('div', UEBERSCHRIFT, T.h_google));\n        if (!b.google) {\n            ziel.appendChild(el('p', ZELLE + ' margin: 0 0 10px;', T.google_aus));\n        } else {\n            var eigene = b.gruppen.hoch.length + b.gruppen.mittel.length;\n            ziel.appendChild(googleBlock(T.google_streng, b.google.streng, eigene));\n            ziel.appendChild(googleBlock(T.google_locker, b.google.locker, eigene));\n            ziel.appendChild(el('p', ZELLE + ' margin: 4px 0 0; line-height: 1.55;', T.google_hinweis));\n        }\n    }\n    window.LW_TEST.MPV.darstellen = darstellen;\n\n    function wert(id) { var e = document.getElementById(id); return e ? e.value : ''; }\n    function angehakt(id) { var e = document.getElementById(id); return !!(e &#038;&#038; e.checked); }\n\n    var b1 = document.getElementById('mpv-bsp1');\n    var b2 = document.getElementById('mpv-bsp2');\n    if (b1) { b1.addEventListener('click', function () { feld.value = BEISPIEL_FEHLER; }); }\n    if (b2) { b2.addEventListener('click', function () { feld.value = BEISPIEL_SAUBER; }); }\n    if (feld.value.trim() === '') { feld.value = BEISPIEL_FEHLER; }\n\n    var laeuft = false;\n    btn.addEventListener('click', function () {\n        if (laeuft) { return; }\n        laeuft = true; btn.disabled = true;\n        out.innerHTML = '';\n        out.appendChild(el('p', ZELLE + GRAU + ' margin: 0;', T.laeuft));\n        abfragen({\n            nutzdaten: feld.value, strom: wert('mpv-strom') === 'app' ? 'app' : 'web',\n            tarif: angehakt('mpv-360') ? '360' : 'standard', google: angehakt('mpv-google')\n        }).then(function (a) { darstellen(bewerten(a), out); laeuft = false; btn.disabled = false; },\n            function () { darstellen(bewerten({ fehler: 'netz' }), out); laeuft = false; btn.disabled = false; });\n    });\n})();\n<\/script><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Reads a Measurement Protocol payload against the documented rules and reports every fault in one pass: reserved names, silent length limits, timestamps in the wrong unit, personal data in parameters, item sums that disagree with value. The same payload also goes to Google\u2019s own validation server twice \u2014 strict and the way production treats it \u2014 so the gap between the three verdicts is visible side by side.<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":38,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"template-tool-base.php","meta":{"footnotes":""},"tags":[91297,91258,91061],"class_list":["post-16774","page","type-page","status-publish","hentry","tag-data-quality","tag-google-analytics-4","tag-server-side-gtm"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.1 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>GA4 Measurement Protocol JSON Validator: see every fault at once - Lukas Wojcik - Blog<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/ga4-measurement-protocol-json-validator\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"GA4 Measurement Protocol JSON Validator: see every fault at once - Lukas Wojcik - Blog\" \/>\n<meta property=\"og:description\" content=\"Reads a Measurement Protocol payload against the documented rules and reports every fault in one pass: reserved names, silent length limits, timestamps in the wrong unit, personal data in parameters, item sums that disagree with value. The same payload also goes to Google\u2019s own validation server twice \u2014 strict and the way production treats it \u2014 so the gap between the three verdicts is visible side by side.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/ga4-measurement-protocol-json-validator\/\" \/>\n<meta property=\"og:site_name\" content=\"Lukas Wojcik - Blog\" \/>\n<meta property=\"article:modified_time\" content=\"2026-09-09T11:09:25+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/08\/og-default.jpg\" \/>\n\t<meta property=\"og:image:width\" content=\"1200\" \/>\n\t<meta property=\"og:image:height\" content=\"630\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data1\" content=\"1 minute\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/ga4-measurement-protocol-json-validator\\\/\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/ga4-measurement-protocol-json-validator\\\/\",\"name\":\"GA4 Measurement Protocol JSON Validator: see every fault at once - Lukas Wojcik - Blog\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#website\"},\"datePublished\":\"2026-09-09T10:40:46+00:00\",\"dateModified\":\"2026-09-09T11:09:25+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/ga4-measurement-protocol-json-validator\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/ga4-measurement-protocol-json-validator\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/ga4-measurement-protocol-json-validator\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Toolbox\",\"item\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/en\\\/toolbox\\\/\"},{\"@type\":\"ListItem\",\"position\":3,\"name\":\"GA4 Measurement Protocol JSON Validator: see every fault at once\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/\",\"name\":\"Lukas Wojcik - Blog\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#\\\/schema\\\/person\\\/895f7604f9b6b71aad9bba33af28d0f9\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":[\"Person\",\"Organization\"],\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#\\\/schema\\\/person\\\/895f7604f9b6b71aad9bba33af28d0f9\",\"name\":\"luky\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\",\"contentUrl\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\",\"width\":424,\"height\":636,\"caption\":\"luky\"},\"logo\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\"},\"sameAs\":[\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"GA4 Measurement Protocol JSON Validator: see every fault at once - Lukas Wojcik - Blog","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/ga4-measurement-protocol-json-validator\/","og_locale":"en_US","og_type":"article","og_title":"GA4 Measurement Protocol JSON Validator: see every fault at once - Lukas Wojcik - Blog","og_description":"Reads a Measurement Protocol payload against the documented rules and reports every fault in one pass: reserved names, silent length limits, timestamps in the wrong unit, personal data in parameters, item sums that disagree with value. The same payload also goes to Google\u2019s own validation server twice \u2014 strict and the way production treats it \u2014 so the gap between the three verdicts is visible side by side.","og_url":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/ga4-measurement-protocol-json-validator\/","og_site_name":"Lukas Wojcik - Blog","article_modified_time":"2026-09-09T11:09:25+00:00","og_image":[{"width":1200,"height":630,"url":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/08\/og-default.jpg","type":"image\/jpeg"}],"twitter_card":"summary_large_image","twitter_misc":{"Est. reading time":"1 minute"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/ga4-measurement-protocol-json-validator\/","url":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/ga4-measurement-protocol-json-validator\/","name":"GA4 Measurement Protocol JSON Validator: see every fault at once - Lukas Wojcik - Blog","isPartOf":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/#website"},"datePublished":"2026-09-09T10:40:46+00:00","dateModified":"2026-09-09T11:09:25+00:00","breadcrumb":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/ga4-measurement-protocol-json-validator\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/ga4-measurement-protocol-json-validator\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/ga4-measurement-protocol-json-validator\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/www.lukaswojcik.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Toolbox","item":"https:\/\/www.lukaswojcik.com\/blog\/en\/toolbox\/"},{"@type":"ListItem","position":3,"name":"GA4 Measurement Protocol JSON Validator: see every fault at once"}]},{"@type":"WebSite","@id":"https:\/\/www.lukaswojcik.com\/blog\/#website","url":"https:\/\/www.lukaswojcik.com\/blog\/","name":"Lukas Wojcik - Blog","description":"","publisher":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/#\/schema\/person\/895f7604f9b6b71aad9bba33af28d0f9"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.lukaswojcik.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":["Person","Organization"],"@id":"https:\/\/www.lukaswojcik.com\/blog\/#\/schema\/person\/895f7604f9b6b71aad9bba33af28d0f9","name":"luky","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg","url":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg","contentUrl":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg","width":424,"height":636,"caption":"luky"},"logo":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg"},"sameAs":["https:\/\/www.lukaswojcik.com\/blog"]}]}},"_links":{"self":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/pages\/16774","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/comments?post=16774"}],"version-history":[{"count":1,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/pages\/16774\/revisions"}],"predecessor-version":[{"id":17026,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/pages\/16774\/revisions\/17026"}],"up":[{"embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/pages\/38"}],"wp:attachment":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/media?parent=16774"}],"wp:term":[{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/tags?post=16774"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}