diff --git a/crates/fmp-py-gen/src/bin/gen_go/models.rs b/crates/fmp-py-gen/src/bin/gen_go/models.rs index 07c3e71b..30ebbab7 100644 --- a/crates/fmp-py-gen/src/bin/gen_go/models.rs +++ b/crates/fmp-py-gen/src/bin/gen_go/models.rs @@ -222,6 +222,7 @@ fn render_model(model: &ModelPlan, out: &mut String) { | Codec::RequiredNumber | Codec::Count | Codec::EmptyOrNullString + | Codec::NullTextString | Codec::EmptyDate | Codec::EmptyOrNullDate => local_name(rust_name), }; @@ -314,6 +315,7 @@ fn render_required_switch(model: &ModelPlan, out: &mut String) { | Codec::RequiredNumber | Codec::Count | Codec::EmptyOrNullString + | Codec::NullTextString | Codec::EmptyDate | Codec::EmptyOrNullDate | Codec::DynamicJson => format!("len(shadow.{}) == 0", field.name), @@ -356,13 +358,18 @@ fn render_codec_block(model: &str, field: &GoField, local: &str, out: &mut Strin field.public_ty, field.name, field.name ); } - Codec::EmptyOrNullString => { + Codec::EmptyOrNullString | Codec::NullTextString => { let inner = field.public_ty.trim_start_matches('*'); + let sentinel = if field.codec == Codec::NullTextString { + "NULL" + } else { + "" + }; let _ = writeln!( out, "\tvar {local} {}\n\tif shadow.{}.Kind() != 'n' {{\n\t\tvar value {inner}\n\ \t\tif err := json.Unmarshal(shadow.{}, &value); err != nil {{\n\ - \t\t\treturn err\n\t\t}}\n\t\tif value != \"\" {{\n\t\t\t{local} = &value\n\t\t}}\n\t}}", + \t\t\treturn err\n\t\t}}\n\t\tif value != {sentinel:?} {{\n\t\t\t{local} = &value\n\t\t}}\n\t}}", field.public_ty, field.name, field.name ); } diff --git a/crates/fmp-py-gen/src/bin/gen_go/tests.rs b/crates/fmp-py-gen/src/bin/gen_go/tests.rs index 3ff4d852..e4b4702d 100644 --- a/crates/fmp-py-gen/src/bin/gen_go/tests.rs +++ b/crates/fmp-py-gen/src/bin/gen_go/tests.rs @@ -697,6 +697,54 @@ fn empty_or_null_codec_decodes_empty_and_null_to_nil_through_the_raw_shadow() { } } +#[test] +fn null_text_codec_decodes_the_null_text_and_null_to_nil_through_the_raw_shadow() { + let aliases = BTreeMap::new(); + let structs = Vec::new(); + let table = TypeTable::new(&structs, &aliases); + let codec = "crate::codecs::null_text::deserialize"; + let def = row(vec![field( + "entity_org_type", + "Option", + deserialize_with(codec), + )]); + let mapped = table.go_field(&def, &def.fields[0]).expect("maps"); + assert_eq!( + ( + mapped.public_ty.as_str(), + mapped.shadow_ty.as_str(), + mapped.codec, + mapped.required_key(), + ), + ("*string", "jsontext.Value", Codec::NullTextString, true) + ); + let models = plan_models("test", &[&def], &table).expect("plans"); + let rendered = render_models("test", &models); + for expected in [ + "EntityOrgType *string `json:\"entityOrgType\"`", + "EntityOrgType jsontext.Value `json:\"entityOrgType\"`", + "case len(shadow.EntityOrgType) == 0:", + "var entityOrgType *string\n\tif shadow.EntityOrgType.Kind() != 'n' {", + "if value != \"NULL\" {\n\t\t\tentityOrgType = &value\n\t\t}", + "EntityOrgType: entityOrgType,", + ] { + assert!( + rendered.contains(expected), + "missing {expected:?} in\n{rendered}" + ); + } + + for (name, ty) in [ + ("bare", "NumericString"), + ("price", "Option"), + ("many", "Option>"), + ] { + let def = row(vec![field(name, ty, deserialize_with(codec))]); + let error = table.go_field(&def, &def.fields[0]).expect_err(name); + assert!(error.contains("null_text"), "{name}: {error}"); + } +} + #[test] fn skip_serializing_if_none_marks_only_the_public_tag_omitzero() { let aliases = BTreeMap::new(); diff --git a/crates/fmp-py-gen/src/bin/gen_go/types.rs b/crates/fmp-py-gen/src/bin/gen_go/types.rs index 904ca3f9..ec27e236 100644 --- a/crates/fmp-py-gen/src/bin/gen_go/types.rs +++ b/crates/fmp-py-gen/src/bin/gen_go/types.rs @@ -40,6 +40,11 @@ pub(crate) enum Codec { /// `Option` of a string-backed type: the key is required, and `""` and /// null are both absent (ADR 0033). The shadow keeps the raw value. EmptyOrNullString, + /// `deserialize_with = "crate::codecs::null_text::deserialize"` on an + /// `Option` of a string-backed type: the key is required, and the text + /// `"NULL"` and null are both absent (ADR 0033). The shadow keeps the raw + /// value. + NullTextString, /// A `DynamicObject` that holds every member the named fields do not /// claim, as serde `flatten` on a map does: `#[serde(flatten)]`, or the /// single `DynamicObject` field of a struct with a hand-written @@ -54,6 +59,9 @@ const COUNT_CODEC: &str = "crate::codecs::count::deserialize"; /// The `deserialize_with` path of the Rust empty-or-null typed-code codec. const EMPTY_OR_NULL_CODEC: &str = "crate::codecs::empty_or_null::deserialize"; +/// The `deserialize_with` path of the Rust `"NULL"`-text codec. +const NULL_TEXT_CODEC: &str = "crate::codecs::null_text::deserialize"; + /// One Go struct member derived from a Rust field. #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct GoField { @@ -88,6 +96,7 @@ impl GoField { | Codec::RequiredNumber | Codec::Count | Codec::EmptyOrNullString + | Codec::NullTextString | Codec::EmptyDate | Codec::EmptyOrNullDate | Codec::DynamicJson @@ -207,6 +216,14 @@ impl<'a> TypeTable<'a> { "the empty_or_null codec needs an Option of a string-backed type".to_string(), )); } + (Some(NULL_TEXT_CODEC), BaseKind::Scalar, [Wrap::Option]) if base.go == "string" => { + (Codec::NullTextString, "jsontext.Value".to_string()) + } + (Some(NULL_TEXT_CODEC), _, _) => { + return Err(fail( + "the null_text codec needs an Option of a string-backed type".to_string(), + )); + } (Some("required_option"), _, _) => { return Err(fail( "required_option needs an Option field" diff --git a/crates/fmp-py/python/fmp/_native/funds/__init__.pyi b/crates/fmp-py/python/fmp/_native/funds/__init__.pyi index 0d1733d3..ba09ff8d 100644 --- a/crates/fmp-py/python/fmp/_native/funds/__init__.pyi +++ b/crates/fmp-py/python/fmp/_native/funds/__init__.pyi @@ -480,7 +480,7 @@ class FundDisclosureSearchResult: @property def entity_name(self) -> builtins.str: ... @property - def entity_org_type(self) -> builtins.str: ... + def entity_org_type(self) -> typing.Optional[builtins.str]: ... @property def series_name(self) -> builtins.str: ... @property @@ -504,7 +504,7 @@ class FundDisclosureSearchResult: class_id: builtins.str, series_id: builtins.str, entity_name: builtins.str, - entity_org_type: builtins.str, + entity_org_type: typing.Optional[builtins.str], series_name: builtins.str, class_name: builtins.str, reporting_file_number: builtins.str, diff --git a/crates/fmp-py/src/models/funds.rs b/crates/fmp-py/src/models/funds.rs index 6280169b..3ef90985 100644 --- a/crates/fmp-py/src/models/funds.rs +++ b/crates/fmp-py/src/models/funds.rs @@ -1616,7 +1616,7 @@ pub(crate) struct FundDisclosureSearchResult { #[pyo3(get)] pub entity_name: String, #[pyo3(get)] - pub entity_org_type: String, + pub entity_org_type: Option, #[pyo3(get)] pub series_name: String, #[pyo3(get)] @@ -1646,7 +1646,7 @@ impl FundDisclosureSearchResult { class_id: String, series_id: String, entity_name: String, - entity_org_type: String, + entity_org_type: Option, series_name: String, class_name: String, reporting_file_number: String, @@ -1834,7 +1834,7 @@ impl From for FundDisclosu class_id: value.class_id, series_id: value.series_id, entity_name: value.entity_name, - entity_org_type: value.entity_org_type.into_inner(), + entity_org_type: value.entity_org_type.map(|value| value.into_inner()), series_name: value.series_name, class_name: value.class_name, reporting_file_number: value.reporting_file_number, diff --git a/crates/fmp-py/tests/test_funds_b.py b/crates/fmp-py/tests/test_funds_b.py index 98e796ef..3259bb69 100644 --- a/crates/fmp-py/tests/test_funds_b.py +++ b/crates/fmp-py/tests/test_funds_b.py @@ -107,6 +107,24 @@ def test_disclosure_rows_decode_omitted_members_as_none(client: Any, fixture_ser assert client.funds.search_disclosure_holders(FEDERATED)[0].address is None +def test_search_entity_org_type_null_text_decodes_as_none( + client: Any, fixture_server: FixtureServer, errors: SimpleNamespace +) -> None: + """The ``"NULL"`` text in ``entityOrgType`` is ``None``; any other non-numeric text is a decode error.""" + search = load_fixture("fund_disclosure_holder_search.json") + search[0]["entityOrgType"] = "NULL" + fixture_server.route("/funds/disclosure-holders-search", search) + assert client.funds.search_disclosure_holders(FEDERATED)[0].entity_org_type is None + + search[0]["entityOrgType"] = "SECRET" + fixture_server.route("/funds/disclosure-holders-search", search) + with pytest.raises(errors.FmpDecodeError) as raised: + client.funds.search_disclosure_holders(FEDERATED) + assert raised.value.endpoint == "funds/disclosure-holders-search" + assert raised.value.decode_path == "[0].entityOrgType" + assert raised.value.decode_kind == "invalid_value" + + def test_search_fund_disclosure_holders_with_a_plain_name(client: Any, fixture_server: FixtureServer) -> None: """``search_disclosure_holders`` sends a space-separated name as ``+`` and nothing else.""" fixture_server.route("/funds/disclosure-holders-search", load_fixture("fund_disclosure_holder_search.json")) diff --git a/crates/libfmp/src/codecs.rs b/crates/libfmp/src/codecs.rs index a660d4d4..f9b31914 100644 --- a/crates/libfmp/src/codecs.rs +++ b/crates/libfmp/src/codecs.rs @@ -807,6 +807,33 @@ pub(crate) mod empty_or_null { } } +/// Deserializer for a required wire key holding a [`NumericString`] whose +/// absent value the provider sends as the text `"NULL"` (ADR 0033, #380). +/// +/// The exact text `"NULL"` and JSON null decode to `None`; any other string +/// must be a numeric string, so `""`, `"null"` or `" NULL"` are still decode +/// errors, and that error never includes the value. The key stays required: +/// do not add `default` at call sites. Apply with +/// `deserialize_with = "crate::codecs::null_text::deserialize"` (the exact +/// path gen_go matches); serialization is serde's default, so `None` +/// re-encodes as null. +pub(crate) mod null_text { + use super::*; + + pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + match Option::::deserialize(deserializer)? { + None => Ok(None), + Some(value) if value == "NULL" => Ok(None), + Some(value) => NumericString::new(value) + .map(Some) + .map_err(de::Error::custom), + } + } +} + /// Serializers for response fields typed with an integral-`f64` alias. /// /// The provider documents fields such as [`Volume`](crate::types::Volume) @@ -967,6 +994,50 @@ mod tests { ); } + #[test] + fn null_text_maps_the_null_text_and_null_to_none_and_parses_the_rest() { + #[derive(Debug, PartialEq, Serialize, Deserialize)] + struct Row { + #[serde(deserialize_with = "super::null_text::deserialize")] + org_type: Option, + } + let decode = |text: &str| serde_json::from_str::(text); + let from_value = + |text: &str| serde_json::from_value::(serde_json::from_str(text).unwrap()); + + for text in [r#"{"org_type":"NULL"}"#, r#"{"org_type":null}"#] { + assert_eq!(decode(text).unwrap(), Row { org_type: None }); + assert_eq!(from_value(text).unwrap(), Row { org_type: None }); + } + let row = decode(r#"{"org_type":"30"}"#).unwrap(); + assert_eq!( + row.org_type.as_ref().map(|value| value.as_str()), + Some("30") + ); + assert_eq!(serde_json::to_string(&row).unwrap(), r#"{"org_type":"30"}"#); + assert_eq!( + serde_json::to_string(&Row { org_type: None }).unwrap(), + r#"{"org_type":null}"# + ); + + for (text, secret) in [ + (r#"{"org_type":"SECRET"}"#, "SECRET"), + (r#"{"org_type":"null"}"#, "null"), + (r#"{"org_type":" NULL"}"#, "NULL"), + (r#"{"org_type":""}"#, "\"\""), + ] { + let error = decode(text).unwrap_err().to_string(); + assert!(from_value(text).is_err(), "from_value {text}"); + assert!( + !error.contains(secret), + "the error must not echo the value: {error}" + ); + } + assert!(decode(r#"{"org_type":30}"#).is_err()); + let missing = decode("{}").unwrap_err().to_string(); + assert!(missing.contains("missing field `org_type`"), "{missing}"); + } + #[test] fn count_accepts_integers_and_integral_floats_on_both_decode_paths() { #[derive(Debug, PartialEq, Serialize, Deserialize)] diff --git a/crates/libfmp/src/responses/funds.rs b/crates/libfmp/src/responses/funds.rs index e2b34d74..1f429b71 100644 --- a/crates/libfmp/src/responses/funds.rs +++ b/crates/libfmp/src/responses/funds.rs @@ -171,7 +171,8 @@ pub struct FundDisclosureSearchResult { pub class_id: String, pub series_id: String, pub entity_name: String, - pub entity_org_type: NumericString, + #[serde(deserialize_with = "crate::codecs::null_text::deserialize")] + pub entity_org_type: Option, pub series_name: String, pub class_name: String, pub reporting_file_number: String, diff --git a/crates/libfmp/tests/fund_disclosure_endpoints.rs b/crates/libfmp/tests/fund_disclosure_endpoints.rs index 3e33f491..ddfb4c50 100644 --- a/crates/libfmp/tests/fund_disclosure_endpoints.rs +++ b/crates/libfmp/tests/fund_disclosure_endpoints.rs @@ -137,7 +137,7 @@ async fn custom_proxy_preserves_exact_query_order_encoding_headers_and_fixture_i ApiDateTime::parse("2023-12-28 09:26:13").unwrap() ); assert_eq!(search[0].cik.as_str(), "0000355691"); - assert_eq!(search[0].entity_org_type.as_str(), "30"); + assert_eq!(search[0].entity_org_type.as_ref().unwrap().as_str(), "30"); assert_eq!(dates[0].year.get(), 2026); assert_eq!(dates[0].quarter.get(), 2); diff --git a/crates/libfmp/tests/funds_responses.rs b/crates/libfmp/tests/funds_responses.rs index 3670ce79..b6be4cb8 100644 --- a/crates/libfmp/tests/funds_responses.rs +++ b/crates/libfmp/tests/funds_responses.rs @@ -218,7 +218,10 @@ fn exact_search_fixture_preserves_all_thirteen_string_fields() { row.entity_name, "Federated Hermes Government Income Securities, Inc." ); - assert_eq!(row.entity_org_type.as_str(), "30"); + assert_eq!( + row.entity_org_type.as_ref().map(NumericString::as_str), + Some("30") + ); assert_eq!( row.series_name, "Federated Hermes Government Income Securities, Inc." @@ -346,7 +349,7 @@ fn all_fields_and_nested_sector_fields_are_required_non_null_and_forward_toleran assert_contract::(SECTOR, &[]); assert_contract::(LATEST_HOLDERS, &[]); assert_contract::(DISCLOSURES, &["symbol", "isin"]); - assert_contract::(SEARCH, &["address"]); + assert_contract::(SEARCH, &["entityOrgType", "address"]); assert_contract::(DATES, &[]); for field in ["industry", "exposure"] { @@ -432,6 +435,20 @@ fn omittable_members_decode_null_and_empty_as_none() { search[0]["address"] = serde_json::json!(""); let rows: Vec = serde_json::from_value(search).unwrap(); assert_eq!(rows[0].address.as_deref(), Some("")); + + let mut search: serde_json::Value = serde_json::from_slice(SEARCH).unwrap(); + search[0]["entityOrgType"] = serde_json::json!("NULL"); + let rows: Vec = serde_json::from_value(search).unwrap(); + assert_eq!(rows[0].entity_org_type, None); + assert!(serde_json::to_value(&rows).unwrap()[0]["entityOrgType"].is_null()); + for wire in ["SECRET", "null", ""] { + let mut search: serde_json::Value = serde_json::from_slice(SEARCH).unwrap(); + search[0]["entityOrgType"] = serde_json::json!(wire); + let error = serde_json::from_value::>(search) + .unwrap_err() + .to_string(); + assert!(!error.contains("SECRET"), "{error}"); + } } fn source_row(fixture: &[u8]) -> serde_json::Value { diff --git a/docs/adr/0033-nullable-response-members.md b/docs/adr/0033-nullable-response-members.md index 1cec8424..61abbf8c 100644 --- a/docs/adr/0033-nullable-response-members.md +++ b/docs/adr/0033-nullable-response-members.md @@ -28,6 +28,7 @@ evidence is now a second source, so both need a rule. | `""` on a typed code (`CountryCode`, `Isin`, `Ticker`, ...) | `Option` through `codecs::empty_or_null` | `None` | `nil` | | `""` on a date | `Option` through `codecs::empty_or_null_date` | `None` | `nil` | | `""` on a plain `String` | unchanged: stays `""` | `""` | `""` | +| `"NULL"` text on a `NumericString` (`FundDisclosureSearchResult.entityOrgType`, #380) | `Option` through `codecs::null_text` | `None` | `nil` | - Evidence decides: a member changes only when the audit (or a later report) saw it null or empty. Parity siblings are not widened. @@ -37,8 +38,14 @@ evidence is now a second source, so both need a rule. value. `None` re-encodes as `null`. - No invented values: a null amount never becomes `0` or `NaN`, and a row is never dropped to hide a bad member. +- `codecs::null_text` accepts the exact text `"NULL"` and null as `None` + and parses any other string as a `NumericString`, so `""`, `"null"`, or a + padded `" NULL"` is still an error that never includes the value. It is + used only where the evidence shows `"NULL"` standing in for an absent + value (the live rows that send it on `entityOrgType` also send a null + `address`); no other codec treats `"NULL"` as absent. - A member whose non-null value has the wrong type (a year where a date is - documented, the text `"NULL"`) is a separate type fix, not an `Option`. + documented) is a separate type fix, not an `Option`. - Every change is breaking and ships in a minor release with a `BREAKING CHANGE:` footer naming the members. @@ -47,8 +54,9 @@ evidence is now a second source, so both need a rule. `gen_go` maps `deserialize_with = "crate::codecs::empty_or_null::deserialize"` on an `Option` of a string-backed type to `Codec::EmptyOrNullString`: the shadow member keeps the raw `jsontext.Value`, the key is required, and -`""` or null decode to `nil`. It fails on any other shape rather than -guessing. +`""` or null decode to `nil`. `crate::codecs::null_text::deserialize` maps +the same way to `Codec::NullTextString`, where the text `"NULL"` or null +decode to `nil`. It fails on any other shape rather than guessing. ## Alternatives considered diff --git a/sdk/go/funds_models.go b/sdk/go/funds_models.go index cab38d97..96fd47bc 100644 --- a/sdk/go/funds_models.go +++ b/sdk/go/funds_models.go @@ -624,7 +624,7 @@ type FundDisclosureSearchResult struct { ClassID string `json:"classId"` SeriesID string `json:"seriesId"` EntityName string `json:"entityName"` - EntityOrgType string `json:"entityOrgType"` + EntityOrgType *string `json:"entityOrgType"` SeriesName string `json:"seriesName"` ClassName string `json:"className"` ReportingFileNumber string `json:"reportingFileNumber"` @@ -643,7 +643,7 @@ type fundDisclosureSearchResultShadow struct { ClassID *string `json:"classId"` SeriesID *string `json:"seriesId"` EntityName *string `json:"entityName"` - EntityOrgType *string `json:"entityOrgType"` + EntityOrgType jsontext.Value `json:"entityOrgType"` SeriesName *string `json:"seriesName"` ClassName *string `json:"className"` ReportingFileNumber *string `json:"reportingFileNumber"` @@ -672,7 +672,7 @@ func (m *FundDisclosureSearchResult) UnmarshalJSONFrom(dec *jsontext.Decoder) er return missingMemberError("FundDisclosureSearchResult", "seriesId") case shadow.EntityName == nil: return missingMemberError("FundDisclosureSearchResult", "entityName") - case shadow.EntityOrgType == nil: + case len(shadow.EntityOrgType) == 0: return missingMemberError("FundDisclosureSearchResult", "entityOrgType") case shadow.SeriesName == nil: return missingMemberError("FundDisclosureSearchResult", "seriesName") @@ -689,6 +689,16 @@ func (m *FundDisclosureSearchResult) UnmarshalJSONFrom(dec *jsontext.Decoder) er case shadow.State == nil: return missingMemberError("FundDisclosureSearchResult", "state") } + var entityOrgType *string + if shadow.EntityOrgType.Kind() != 'n' { + var value string + if err := json.Unmarshal(shadow.EntityOrgType, &value); err != nil { + return err + } + if value != "NULL" { + entityOrgType = &value + } + } var address *string if shadow.Address.Kind() != 'n' { var value string @@ -703,7 +713,7 @@ func (m *FundDisclosureSearchResult) UnmarshalJSONFrom(dec *jsontext.Decoder) er ClassID: *shadow.ClassID, SeriesID: *shadow.SeriesID, EntityName: *shadow.EntityName, - EntityOrgType: *shadow.EntityOrgType, + EntityOrgType: entityOrgType, SeriesName: *shadow.SeriesName, ClassName: *shadow.ClassName, ReportingFileNumber: *shadow.ReportingFileNumber, diff --git a/sdk/go/funds_parity_test.go b/sdk/go/funds_parity_test.go index 95e691eb..2b42830f 100644 --- a/sdk/go/funds_parity_test.go +++ b/sdk/go/funds_parity_test.go @@ -133,7 +133,7 @@ func TestDocumentedFundDisclosureFixturesDecodeExactly(t *testing.T) { results := assertFixtureParity[FundDisclosureSearchResult](t, "fund_disclosure_holder_search.json") wantResult := FundDisclosureSearchResult{ Symbol: "FGOAX", CIK: "0000355691", ClassID: "C000024574", SeriesID: "S000009042", - EntityName: "Federated Hermes Government Income Securities, Inc.", EntityOrgType: "30", + EntityName: "Federated Hermes Government Income Securities, Inc.", EntityOrgType: new("30"), SeriesName: "Federated Hermes Government Income Securities, Inc.", ClassName: "Class A Shares", ReportingFileNumber: "811-03266", Address: new("4000 ERICSSON DRIVE"), City: "WARRENDALE", ZipCode: "15086-7561", State: "PA", @@ -208,6 +208,17 @@ func TestFundsOmittableMembersDecodeNullAndEmptyAsNil(t *testing.T) { map[string]string{"address": `""`}), &results); err != nil || !reflect.DeepEqual(results[0].Address, new("")) { t.Fatalf("search empty address = %+v, %v", results, err) } + for _, wire := range []string{`"NULL"`, "null"} { + if err := json.Unmarshal(fundsRewrite(t, "fund_disclosure_holder_search.json", + map[string]string{"entityOrgType": wire}), &results); err != nil || results[0].EntityOrgType != nil || + string(memberValues(t, results[0])["entityOrgType"]) != "null" { + t.Fatalf("search entityOrgType %s = %+v, %v", wire, results, err) + } + } + if err := json.Unmarshal(fundsRewrite(t, "fund_disclosure_holder_search.json", + map[string]string{"entityOrgType": "30"}), &results); err == nil || strings.Contains(err.Error(), "30") { + t.Fatalf("search numeric entityOrgType = %+v, %v, want a type error without the value", results, err) + } } // Mirrors integer_widths_signed_change_and_decimal_market_values_are_preserved. diff --git a/sdk/go/funds_test.go b/sdk/go/funds_test.go index 0b08d176..85f179a8 100644 --- a/sdk/go/funds_test.go +++ b/sdk/go/funds_test.go @@ -81,7 +81,7 @@ func TestFundsMethodsUseExactPathsAndWireOrder(t *testing.T) { } results, err := ns.SearchDisclosureHolders(ctx, NewFundDisclosureHolderSearchQuery("Federated Hermes Government Income Securities, Inc.")) - if err != nil || len(results) != 1 || results[0].EntityOrgType != "30" { + if err != nil || len(results) != 1 || results[0].EntityOrgType == nil || *results[0].EntityOrgType != "30" { t.Fatalf("SearchDisclosureHolders = %+v, %v", results, err) } dates, err := ns.DisclosureDates(ctx, NewFundDisclosureDatesQuery("VWO").WithCIK("0000036405"))