Terraform Plugin Framework: Value Conversion Error nested objects

Im currently working on creating a terraform provider to create webhooks using the Adyen API. I have issues converting the HTTP 200 response from Adyen to a valid Schema.

The response from Adyen looks like this:

{
    "id": "some-id",
    "type": "standard",
    "url": "https://webhook.site/some-webhook",
    "description": "Some description",
    "username": "YOUR_USER",
    "hasPassword": true,
    "active": true,
    "hasError": false,
    "encryptionProtocol": "TLSv1.3",
    "communicationFormat": "json",
    "acceptsExpiredCertificate": false,
    "acceptsSelfSignedCertificate": true,
    "acceptsUntrustedRootCertificate": true,
    "certificateAlias": "signed-test.adyen.com_2023",
    "populateSoapActionHeader": false,
    "additionalSettings": {
        "properties": {
            "includePosTerminalInfo": false,
            "includeARN": false,
            "includeExtraCostsSurcharge": false,
            "includePaymentAccountOwnerDetails": false,
            "includePosDetails": false,
            "includeCardInfoForRecurringContractNotification": false,
            "includeRiskData": false,
            "includeRiskExperimentReference": false,
            "includeSoapSecurityHeader": false,
            "includeContactlessWalletTokenInformation": false,
            "includeExtraCostsGratuity": false,
            "includeAdjustAuthorisationData": false,
            "includeAcquirerReference": false,
            "includeRiskProfileReference": false,
            "includeOriginalMerchantReferenceCancelOrRefundNotification": false,
            "includeNfcTokenInformation": false,
            "includeSubvariant": false,
            "includeThreeDSVersion": false,
            "includeInstallmentsInfo": false,
            "includeAliasInfo": false,
            "includeShopperCountry": false,
            "includeRawThreeDSecureResult": false,
            "includeAirlineData": false,
            "includeGrossCurrencyChargebackDetails": false,
            "includeThreeDSecureResult": false,
            "includeMetadataIn3DSecurePaymentNotification": false,
            "includeGiftCardNumber": false,
            "includeOriginalReferenceForChargebackReversed": false,
            "addAcquirerResult": false,
            "includeDeliveryAddress": false,
            "includeRetryAttempts": false,
            "includeVisaRdrDisputeIndicator": false,
            "includeExtraCosts": false,
            "includeCardHolderName": false,
            "includeShopperDetail": false,
            "includeBankAccountDetails": false,
            "includeMandateDetails": false,
            "includeAuthAmountForDynamicZeroAuth": false,
            "includeIssuerCountry": false,
            "includeAcquirerErrorDetails": false,
            "includeCoBrandedWith": false,
            "includeShopperInteraction": false,
            "includeDeviceAndBrowserInfo": false,
            "includeUpiVpa": false,
            "includePixEndToEndId": false,
            "addRawAcquirerResult": false,
            "includeCardBin": false,
            "includeFundingSource": false,
            "includeThreeDS2ChallengeInformation": false,
            "includeRiskProfile": false,
            "includeRealtimeAccountUpdaterStatus": false,
            "includePixPayerInfo": false,
            "includeDunningProjectData": false,
            "includePaymentResultInOrderClosedNotification": false,
            "includeCardBinDetails": false,
            "includeNotesInManualReviewNotifications": false,
            "includeZeroAuthFlag": false,
            "addCaptureReferenceToDisputeNotification": false,
            "includePayPalDetails": false,
            "includeRawThreeDSecureDetailsResult": false,
            "includeBankVerificationResults": false,
            "includeCaptureDelayHours": false,
            "addPaymentAccountReference": false,
            "includePayULatamDetails": false,
            "includeStore": false,
            "returnAvsData": false,
            "includeWeChatPayOpenid": false,
            "includeCustomRoutingFlagging": false,
            "includeTokenisedPaymentMetaData": false
        },
        "includeEventCodes": [
            "CAPTURE_FAILED",
            "CHARGEBACK_REVERSED",
            "REQUEST_FOR_INFORMATION",
            "MANUAL_REVIEW_REJECT",
            "PAIDOUT_REVERSED",
            "PAYOUT_THIRDPARTY",
            "NOTIFICATION_OF_CHARGEBACK",
            "CANCELLATION",
            "REFUNDED_REVERSED",
            "MANUAL_REVIEW_ACCEPT",
            "TECHNICAL_CANCEL",
            "NOTIFICATION_OF_FRAUD",
            "RECURRING_CONTRACT",
            "PAYOUT_EXPIRE",
            "CANCEL_AUTORESCUE",
            "POSTPONED_REFUND",
            "CANCEL_OR_REFUND",
            "CAPTURE",
            "ORDER_CLOSED",
            "REFUND",
            "REFUND_WITH_DATA",
            "AUTHORISATION_ADJUSTMENT",
            "HANDLED_EXTERNALLY",
            "PENDING",
            "REFUND_FAILED",
            "AUTORESCUE",
            "CHARGEBACK",
            "VOID_PENDING_REFUND",
            "PREARBITRATION_WON",
            "ORDER_OPENED",
            "PAYOUT_DECLINE",
            "SECOND_CHARGEBACK",
            "AUTHORISATION",
            "REPORT_AVAILABLE",
            "PREARBITRATION_LOST"
        ],
        "excludeEventCodes": [
            "DONATION",
            "DISPUTE_OPENED_WITH_CHARGEBACK",
            "AUTHENTICATION",
            "ISSUER_COMMENTS",
            "EXPIRE",
            "AUTORESCUE_NEXT_ATTEMPT",
            "OFFER_CLOSED",
            "DISPUTE_DEFENSE_PERIOD_ENDED",
            "ADVICE_OF_DEBIT",
            "INFORMATION_SUPPLIED",
            "ISSUER_RESPONSE_TIMEFRAME_EXPIRED",
            "DISABLE_RECURRING"
        ]
    },
    "_links": {
        "self": {
            "href": "https://management-test.adyen.com/v3/merchants/some-href"
        },
        "generateHmac": {
            "href": "https://management-test.adyen.com/v3/merchants/some-href"
        },
        "merchant": {
            "href": "https://management-test.adyen.com/v3/merchants/some-href"
        },
        "testWebhook": {
            "href": "https://management-test.adyen.com/v3/merchants/some-href"
        }
    }
}

And currently my terraform schema looks like this:

func (r *webhookMerchantResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
	resp.Schema = schema.Schema{
		Attributes: map[string]schema.Attribute{
			"webhooks_merchant": schema.SingleNestedAttribute{
				Required: true,
				Attributes: map[string]schema.Attribute{
					"id": schema.StringAttribute{
						Computed:    true,
						Description: "The unique identifier for the webhook.",
					},
					"type": schema.StringAttribute{
						Required:    true,
						Description: "The type of the webhook.",
					},
					"url": schema.StringAttribute{
						Required:    true,
						Description: "The URL the webhook will send requests to.",
					},
					"username": schema.StringAttribute{
						Optional:    true,
						Description: "The username required for basic authentication.",
					},
					"password": schema.StringAttribute{
						Optional:    true,
						Description: "The password required for basic authentication.",
					},
					"has_password": schema.BoolAttribute{
						Computed:    true,
						Description: "Indicates if the webhook is configured with a password.",
					},
					"active": schema.BoolAttribute{
						Required:    true,
						Description: "Indicates if the webhook is active.",
					},
					"communication_format": schema.StringAttribute{
						Required:    true,
						Description: "The format of the communication (e.g., 'json').",
					},
					"description": schema.StringAttribute{
						Optional:    true,
						Description: "A description of the webhook.",
					},
					"encryption_protocol": schema.StringAttribute{
						Optional:    true,
						Description: "The encryption protocol used by the webhook.",
					},
					"has_error": schema.BoolAttribute{
						Computed:    true,
						Description: "Indicates if there is an error with the webhook.",
					},
					"certificate_alias": schema.StringAttribute{
						Optional:    true,
						Description: "The alias of the certificate.",
					},
					"populate_soap_action_header": schema.BoolAttribute{
						Optional:    true,
						Description: "Indicates if the SOAP action header should be populated.",
					},
					"accepts_expired_certificate": schema.BoolAttribute{
						Required:    true,
						Description: "Indicates if expired certificates are accepted.",
					},
					"accepts_self_signed_certificate": schema.BoolAttribute{
						Required:    true,
						Description: "Indicates if self-signed certificates are accepted.",
					},
					"accepts_untrusted_root_certificate": schema.BoolAttribute{
						Required:    true,
						Description: "Indicates if untrusted root certificates are accepted.",
					},
					"additional_settings": schema.MapAttribute{
						Computed:    true,
						ElementType: types.BoolType,
					},
					"links": schema.SingleNestedAttribute{
						Computed: true,
						Attributes: map[string]schema.Attribute{
							"self": schema.StringAttribute{
								Computed:    true,
								Description: "The API URL to the webhook itself.",
							},
							"generate_hmac": schema.SingleNestedAttribute{
								Computed:    true,
								Description: "The API URL to generate an HMAC key for the webhook.",
							},
							"merchant": schema.SingleNestedAttribute{
								Computed:    true,
								Description: "The API URL to the merchant account associated with the webhook.",
							},
							"test_webhook": schema.SingleNestedAttribute{
								Computed:    true,
								Description: "The API URL to test the webhook.",
							},
						},
					},
				},
			},
		},
	}
}

The error (I also get a similar error for the addtional_settings property):

│ Error: Value Conversion Error
│ 
│   with adyen_webhooks_merchant.example_webhook,
│   on main.tf line 23, in resource "adyen_webhooks_merchant" "example_webhook":
│   23: resource "adyen_webhooks_merchant" "example_webhook" {
│ 
│ An unexpected error was encountered trying to build a value. This is always an error in the provider. Please report the following to the provider developer:
│ 
│ Received unknown value, however the target type cannot handle unknown values. Use the corresponding `types` package type or a custom type that handles unknown values.
│ 
│ Path: webhooks_merchant.links
│ Target Type: webhooks.linksModel
│ Suggested Type: basetypes.ObjectValue
╵

And lastly my go structs look like this:


type webhooksMerchantModel struct {
	ID                              types.String `tfsdk:"id"`
	Type                            types.String `tfsdk:"type"`
	URL                             types.String `tfsdk:"url"`
	Username                        types.String `tfsdk:"username"`
	Description                     types.String `tfsdk:"description"`
	HasPassword                     types.Bool   `tfsdk:"has_password"`
	Password                        types.String `tfsdk:"password"`
	Active                          types.Bool   `tfsdk:"active"`
	HasError                        types.Bool   `tfsdk:"has_error"`
	EncryptionProtocol              types.String `tfsdk:"encryption_protocol"`
	CommunicationFormat             types.String `tfsdk:"communication_format"`
	AcceptsExpiredCertificate       types.Bool   `tfsdk:"accepts_expired_certificate"`
	AcceptsSelfSignedCertificate    types.Bool   `tfsdk:"accepts_self_signed_certificate"`
	AcceptsUntrustedRootCertificate types.Bool   `tfsdk:"accepts_untrusted_root_certificate"`
	CertificateAlias                types.String `tfsdk:"certificate_alias"`
	PopulateSoapActionHeader        types.Bool   `tfsdk:"populate_soap_action_header"`
	AdditionalSettings              types.Map    `tfsdk:"additional_settings"`
	Links                           linksModel   `tfsdk:"links"`
}

type linksModel struct {
	Self         types.String `tfsdk:"self"`
	GenerateHmac types.String `tfsdk:"generate_hmac"`
	Merchant     types.String `tfsdk:"merchant"`
	TestWebhook  types.String `tfsdk:"test_webhook"`
}

I have been looking through the docs for the plugin framework, but couldnt find what terraform types are suited for the additionalSettings and _links.

Thanks in advance!

Hi @tolga,
Sorry that you are running into trouble here. The schema that you have defined for links looks correct, but the webhooksMerchantModel struct needs to define the Links field as a types.Object instead of linksModel.

Links                           types.Object   `tfsdk:"links"`
}

Generally, when you are defining Go structs for provider development, you need to use the framework value types instead of Go types because the Go types cannot handle certain Terraform concepts such as Unknown. The attribute documentation has more information on what underlying framework value types are associated with specific attribute types.

You will then need to convert the object from the framework type to the Go type in your provider code to access its fields.

var config webhooksMerchantModel

// populate the webhooksMerchantModel type with the config
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
	return
}

var linksAttribute linksModel

//populate the linksModel type
diags := config.Links.As(ctx, &linksAttribute, basetypes.ObjectAsOptions{})

//get the "self" nested attribute
self := Links.Self.StringValue()

The object type documentation has more info on how to convert values to and from the framework types.Object.

@SBGoods Thanks for your reply! I will definitely check this out and let you know how it went!

Hi, here again with an update. I opted for using this pattern instead (provided in the docs):

elementTypes := map[string]attr.Type{
    "attr1": types.StringType,
    "attr2": types.Int64Type,
}
elements := map[string]attr.Value{
    "attr1": types.StringValue("value"),
    "attr2": types.Int64Value(123),
}
objectValue, diags := types.ObjectValue(elementTypes, elements)

It does create some overhead which I didn’t expect, since the tutorial for the plugin framework has multiple instances of using Go structs (e.g. hashicups data source):

// coffeesDataSourceModel maps the data source schema data.
type coffeesDataSourceModel struct {
  Coffees []coffeesModel `tfsdk:"coffees"`
}

// coffeesModel maps coffees schema data.
type coffeesModel struct {
  ID          types.Int64               `tfsdk:"id"`
  Name        types.String              `tfsdk:"name"`
  Teaser      types.String              `tfsdk:"teaser"`
  Description types.String              `tfsdk:"description"`
  Price       types.Float64             `tfsdk:"price"`
  Image       types.String              `tfsdk:"image"`
  Ingredients []coffeesIngredientsModel `tfsdk:"ingredients"`
}

// coffeesIngredientsModel maps coffee ingredients data
type coffeesIngredientsModel struct {
  ID types.Int64 `tfsdk:"id"`
}

Thanks for your insight! @SBGoods

1 Like

Hi @tolga,

There are limited cases where using a native Go type (rather than a Terraform type) is okay. The []coffeesIngredientsModel example is probably okay because it can never be null (all coffees have ingredients) and it can never be unknown (it must be supplied directly by the user).

I’ve been down the road of trying to make the right judgement call about using native Go types vs. terraform types depending on the context and eventually concluded it wasn’t worth my sanity.

Every struct element in my provider (which has lots of complicated/nested stuff going on) is a types.Something.

1 Like

@hQnVyLRx Yeah, makes sense! :slight_smile:

Implementing the data source was relatively straightforward, but I had a lot of trouble with unknown values in the resource.