@@ -721,10 +721,14 @@ impl ShopifyFunctionCodeGenerator {
721721/// 1. The field's value is explicitly `null` in the JSON
722722/// 2. The field is missing entirely from the JSON object
723723///
724+ /// - `#[shopify_function(rename = "custom_name")]` - When applied to a field, uses the specified
725+ /// custom name for deserialization instead of the field's Rust name. This takes precedence over
726+ /// any struct-level `rename_all` attribute.
727+ ///
724728/// This is similar to serde's `#[serde(default)]` attribute, allowing structs to handle missing or null
725729/// fields gracefully by using their default values instead of returning an error.
726730///
727- /// Note: Fields that use `#[shopify_function(default)]` must be a type that implements the `Default` trait.
731+ /// Note: Fields that use `#[shopify_function(default)]` must be a type that implements the `Default` trait.
728732#[ proc_macro_derive( Deserialize , attributes( shopify_function) ) ]
729733pub fn derive_deserialize ( input : proc_macro:: TokenStream ) -> proc_macro:: TokenStream {
730734 let input = syn:: parse_macro_input!( input as syn:: DeriveInput ) ;
@@ -734,6 +738,34 @@ pub fn derive_deserialize(input: proc_macro::TokenStream) -> proc_macro::TokenSt
734738 . unwrap_or_else ( |error| error. to_compile_error ( ) . into ( ) )
735739}
736740
741+ #[ derive( Default ) ]
742+ struct FieldAttributes {
743+ rename : Option < String > ,
744+ has_default : bool ,
745+ }
746+
747+ fn parse_field_attributes ( field : & syn:: Field ) -> syn:: Result < FieldAttributes > {
748+ let mut attributes = FieldAttributes :: default ( ) ;
749+
750+ for attr in field. attrs . iter ( ) {
751+ if attr. path ( ) . is_ident ( "shopify_function" ) {
752+ attr. parse_nested_meta ( |meta| {
753+ if meta. path . is_ident ( "rename" ) {
754+ attributes. rename = Some ( meta. value ( ) ?. parse :: < syn:: LitStr > ( ) ?. value ( ) ) ;
755+ Ok ( ( ) )
756+ } else if meta. path . is_ident ( "default" ) {
757+ attributes. has_default = true ;
758+ Ok ( ( ) )
759+ } else {
760+ Err ( meta. error ( "unrecognized field attribute" ) )
761+ }
762+ } ) ?;
763+ }
764+ }
765+
766+ Ok ( attributes)
767+ }
768+
737769fn derive_deserialize_for_derive_input ( input : & syn:: DeriveInput ) -> syn:: Result < syn:: ItemImpl > {
738770 match & input. data {
739771 syn:: Data :: Struct ( data) => match & data. fields {
@@ -775,33 +807,28 @@ fn derive_deserialize_for_derive_input(input: &syn::DeriveInput) -> syn::Result<
775807 . iter ( )
776808 . map ( |field| {
777809 let field_name_ident = field. ident . as_ref ( ) . expect ( "Named fields must have identifiers" ) ;
778- let field_name_str = case_style. map_or_else ( || field_name_ident. to_string ( ) , |case_style| {
779- field_name_ident. to_string ( ) . to_case ( case_style)
780- } ) ;
781- let field_name_lit_str = syn:: LitStr :: new ( field_name_str. as_str ( ) , Span :: mixed_site ( ) ) ;
782810
783- // Check if field has #[shopify_function(default)] attribute
784- let has_default = field. attrs . iter ( ) . any ( |attr| {
785- if attr. path ( ) . is_ident ( "shopify_function" ) {
786- let mut found = false ;
787- let _ = attr. parse_nested_meta ( |meta| {
788- if meta. path . is_ident ( "default" ) {
789- found = true ;
790- }
791- Ok ( ( ) )
792- } ) ;
793- found
794- } else {
795- false
811+ let field_attrs = parse_field_attributes ( field) ?;
812+
813+ let field_name_str = match field_attrs. rename {
814+ Some ( custom_name) => custom_name,
815+ None => {
816+ // Fall back to rename_all case transformation or original name
817+ case_style. map_or_else (
818+ || field_name_ident. to_string ( ) ,
819+ |case_style| field_name_ident. to_string ( ) . to_case ( case_style)
820+ )
796821 }
797- } ) ;
822+ } ;
798823
799- if has_default {
824+ let field_name_lit_str = syn:: LitStr :: new ( field_name_str. as_str ( ) , Span :: mixed_site ( ) ) ;
825+
826+ if field_attrs. has_default {
800827 // For fields with default attribute, check if value is null or missing
801828 // This will use the Default implementation for the field type when either:
802829 // 1. The field is explicitly null in the JSON (we get NanBox::null())
803830 // 2. The field is missing in the JSON (get_obj_prop returns a null value)
804- parse_quote ! {
831+ Ok ( parse_quote ! {
805832 #field_name_ident: {
806833 let prop = value. get_obj_prop( #field_name_lit_str) ;
807834 if prop. is_null( ) {
@@ -810,15 +837,15 @@ fn derive_deserialize_for_derive_input(input: &syn::DeriveInput) -> syn::Result<
810837 shopify_function:: wasm_api:: Deserialize :: deserialize( & prop) ?
811838 }
812839 }
813- }
840+ } )
814841 } else {
815842 // For fields without default, use normal deserialization
816- parse_quote ! {
843+ Ok ( parse_quote ! {
817844 #field_name_ident: shopify_function:: wasm_api:: Deserialize :: deserialize( & value. get_obj_prop( #field_name_lit_str) ) ?
818- }
845+ } )
819846 }
820847 } )
821- . collect ( ) ;
848+ . collect :: < syn :: Result < Vec < _ > > > ( ) ? ;
822849
823850 let deserialize_impl = parse_quote ! {
824851 impl shopify_function:: wasm_api:: Deserialize for #name_ident {
0 commit comments